Merge branch 'master' into story_61

Also got client-server handshake working. Multiple clients can connect and control their own boat.
The client's boat has a larger black triangle drawn around it. Probably needs tidying up.
Added isClientBoat boolean to VisualiserBoat.

WindGenerator is now an interface, implemented by ConstantWindGenerator (useful for testing) and RandomWindGenerator. Added ConstantWindGeneratorTest.
RandomWindGenerator was formerly WindGenerator.
The WindGenerator is passed in to MockRace.

CommandFactory throws a CommandConstructionException if it cannot create a command.

MessageSerialiser and MessageDeserialiser correctly terminate on error.

Readded VisualiserInput's switch statement in the main loop, pending a refactor.

Removed the sleep statement from LobbyController - it was blocking javaFX thread.

Lobby.fxml: moved buttons and text input into grid panes, so they don't break when you resize screen.

Added some test object creation functions in several test classes (MockRaceTest.createMockRace(), BoatXMLReaderTest.createBoatDataSource(), etc...).

#story[1095]
This commit is contained in:
fjc40
2017-08-10 23:49:21 +12:00
45 changed files with 1320 additions and 556 deletions
@@ -19,6 +19,7 @@ import java.net.URL;
import java.net.UnknownHostException;
import java.util.ResourceBundle;
//TODO it appears this view/controller was replaced by Lobby.fxml. Remove?
/**
* Controls the connection that the VIsualiser can connect to.
*/
@@ -82,11 +83,6 @@ public class ConnectionController extends Controller {
private ObservableList<RaceConnection> connections;
/**
* Represents whether the client is currently hosting a game already - this is to ensure they don't launch multiple servers.
*/
private boolean currentlyHostingGame = false;
@Override
public void initialize(URL location, ResourceBundle resources) {
@@ -47,7 +47,7 @@ public class HostController extends Controller {
*/
public void hostGamePressed() throws IOException{
try {
Event game = new Event();
Event game = new Event(true);
game.start();
connectSocket("localhost", 4942);
} catch (EventConstructionException e) {
@@ -78,7 +78,6 @@ public class HostController extends Controller {
*/
public void hostGame(){
hostWrapper.setVisible(true);
System.out.println("Reacted hostGame");
}
}
@@ -58,7 +58,6 @@ public class LobbyController extends Controller {
}
else {
joinGameBtn.setDisable(true);
System.out.println(curr.statusProperty().getValue());
}
});
joinGameBtn.setDisable(true);
@@ -70,11 +69,6 @@ public class LobbyController extends Controller {
public void refreshBtnPressed(){
for(RaceConnection connection: connections) {
connection.check();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
if (lobbyTable.getSelectionModel().getSelectedItem().statusProperty().getValue().equals("Ready")) {
@@ -5,6 +5,7 @@ import javafx.fxml.FXML;
import javafx.scene.layout.AnchorPane;
import visualiser.app.VisualiserInput;
import visualiser.gameController.ControllerClient;
import visualiser.model.ServerConnection;
import visualiser.model.VisualiserBoat;
import visualiser.model.VisualiserRace;
@@ -36,12 +37,12 @@ public class MainController extends Controller {
/**
* Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race).
* @param visualiserInput The object used to read packets from the race server.
* @param visualiserRace The object modelling the race.
* @param controllerClient Socket Client that manipulates the controller.
* @param serverConnection The connection to the server.
*/
public void beginRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace, ControllerClient controllerClient) {
raceController.startRace(visualiserInput, visualiserRace, controllerClient);
public void beginRace(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) {
raceController.startRace(visualiserRace, controllerClient, serverConnection);
}
/**
@@ -103,10 +104,15 @@ public class MainController extends Controller {
AnchorPane.setLeftAnchor(startController.startWrapper(), 0.0);
AnchorPane.setRightAnchor(startController.startWrapper(), 0.0);
AnchorPane.setTopAnchor(connectionController.startWrapper(), 0.0);
AnchorPane.setBottomAnchor(connectionController.startWrapper(), 0.0);
AnchorPane.setLeftAnchor(connectionController.startWrapper(), 0.0);
AnchorPane.setRightAnchor(connectionController.startWrapper(), 0.0);
AnchorPane.setTopAnchor(lobbyController.startWrapper(), 0.0);
AnchorPane.setBottomAnchor(lobbyController.startWrapper(), 0.0);
AnchorPane.setLeftAnchor(lobbyController.startWrapper(), 0.0);
AnchorPane.setRightAnchor(lobbyController.startWrapper(), 0.0);
AnchorPane.setTopAnchor(hostController.startWrapper(), 0.0);
AnchorPane.setBottomAnchor(hostController.startWrapper(), 0.0);
AnchorPane.setLeftAnchor(hostController.startWrapper(), 0.0);
AnchorPane.setRightAnchor(hostController.startWrapper(), 0.0);
AnchorPane.setTopAnchor(finishController.finishWrapper, 0.0);
AnchorPane.setBottomAnchor(finishController.finishWrapper, 0.0);
@@ -34,10 +34,6 @@ import java.util.logging.Logger;
*/
public class RaceController extends Controller {
/**
* The object used to read packets from the connected server.
*/
private VisualiserInput visualiserInput;
/**
* The race object which describes the currently occurring race.
@@ -69,6 +65,11 @@ public class RaceController extends Controller {
*/
private ControllerClient controllerClient;
/**
* The connection to the server.
*/
private ServerConnection serverConnection;
@FXML private GridPane canvasBase;
@@ -351,15 +352,15 @@ public class RaceController extends Controller {
/**
* Displays a specified race.
* @param visualiserInput Object used to read packets from server.
* @param visualiserRace Object modelling the race.
* @param controllerClient Socket Client that manipulates the controller.
* @param serverConnection The connection to the server.
*/
public void startRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace, ControllerClient controllerClient) {
public void startRace(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) {
this.visualiserInput = visualiserInput;
this.visualiserRace = visualiserRace;
this.controllerClient = controllerClient;
this.serverConnection = serverConnection;
initialiseRace();
@@ -420,6 +421,15 @@ public class RaceController extends Controller {
}
//Return to main screen if we lose connection.
if (!serverConnection.isAlive()) {
race.setVisible(false);
parent.enterTitle();
//TODO currently this doesn't work correctly - the title screen remains visible after clicking join game
//TODO we should display an error to the user
//TODO also need to "reset" any state (race, connections, etc...).
}
}
}.start();
}
@@ -74,6 +74,11 @@ public class StartController extends Controller implements Observer {
*/
private ServerConnection serverConnection;
/**
* Writes BoatActions to outgoing message queue.
*/
private ControllerClient controllerClient;
/**
* The race object which describes the currently occurring race.
@@ -85,7 +90,7 @@ public class StartController extends Controller implements Observer {
/**
* An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor.
*/
List<Color> colors = new ArrayList<>(Arrays.asList(
private List<Color> colors = new ArrayList<>(Arrays.asList(
Color.BLUEVIOLET,
Color.BLACK,
Color.RED,
@@ -130,6 +135,14 @@ public class StartController extends Controller implements Observer {
this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors);
new Thread(this.visualiserRace).start();
//Ugly. TODO refactor
ObservableList<VisualiserBoat> boats = visualiserRace.getBoats();
for (VisualiserBoat boat : boats) {
if (boat.getSourceID() == serverConnection.getAllocatedSourceID()) {
boat.setClientBoat(true);
}
}
//Initialise the boat table.
initialiseBoatTable(this.visualiserRace);
@@ -264,7 +277,7 @@ public class StartController extends Controller implements Observer {
startWrapper.setVisible(false);
start.setVisible(false);
parent.beginRace(visualiserInput, visualiserRace, controllerClient);
parent.beginRace(visualiserRace, controllerClient, serverConnection);
}
}
}.start();
@@ -315,6 +328,7 @@ public class StartController extends Controller implements Observer {
LatestMessages latestMessages = new LatestMessages();
this.serverConnection = new ServerConnection(socket, latestMessages);
this.controllerClient = serverConnection.getControllerClient();
//Store a reference to latestMessages so that we can observe it.
@@ -1,17 +1,11 @@
package visualiser.app;
import network.BinaryMessageDecoder;
import network.Exceptions.InvalidMessageException;
import network.Messages.*;
import shared.model.RunnableWithFramePeriod;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import static network.Utils.ByteConverter.bytesToShort;
/**
* TCP client which receives packets/messages from a race data source
@@ -29,7 +23,6 @@ public class VisualiserInput implements RunnableWithFramePeriod {
private long lastHeartbeatSequenceNum = -1;
/**
* Incoming messages from server.
*/
@@ -45,7 +38,8 @@ public class VisualiserInput implements RunnableWithFramePeriod {
/**
* Constructs a visualiserInput to convert an incoming stream of messages into LatestMessages.
* @param latestMessages Object to place messages in.
*
* @param latestMessages Object to place messages in.
* @param incomingMessages The incoming queue of messages.
*/
public VisualiserInput(LatestMessages latestMessages, BlockingQueue<AC35Data> incomingMessages) {
@@ -58,6 +52,7 @@ public class VisualiserInput implements RunnableWithFramePeriod {
/**
* Returns the LatestMessages object, which can be queried for any received race related messages.
*
* @return The LatestMessages object.
*/
public LatestMessages getLatestMessages() {
@@ -66,6 +61,7 @@ public class VisualiserInput implements RunnableWithFramePeriod {
/**
* Calculates the time since last heartbeat, in milliseconds.
*
* @return Time since last heartbeat, in milliseconds..
*/
private double timeSinceHeartbeat() {
@@ -74,22 +70,176 @@ public class VisualiserInput implements RunnableWithFramePeriod {
}
@Override
public void run() {
//Handshake.
while (!Thread.interrupted()) {
//Main loop.
// take message
// create command
// place in command queue
AC35Data message = null;
try {
message = incomingMessages.take();
} catch (InterruptedException e) {
Logger.getGlobal().log(Level.SEVERE, "VisualiserInput was interrupted on thread: " + Thread.currentThread() + " while waiting for messages.");
Thread.currentThread().interrupt();
return;
}
//TODO refactor below
//Checks which message is being received and does what is needed for that message.
switch (message.getType()) {
//Heartbeat.
case HEARTBEAT: {
HeartBeat heartBeat = (HeartBeat) message;
//Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time.
if (heartBeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) {
lastHeartbeatTime = System.currentTimeMillis();
lastHeartbeatSequenceNum = heartBeat.getSequenceNumber();
//System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum);
}
break;
}
//RaceStatus.
case RACESTATUS: {
RaceStatus raceStatus = (RaceStatus) message;
//System.out.println("Race Status Message");
this.latestMessages.setRaceStatus(raceStatus);
for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) {
this.latestMessages.setBoatStatus(boatStatus);
}
break;
}
//DisplayTextMessage.
case DISPLAYTEXTMESSAGE: {
//System.out.println("Display Text Message");
//No decoder for this.
break;
}
//XMLMessage.
case XMLMESSAGE: {
XMLMessage xmlMessage = (XMLMessage) message;
//System.out.println("XML Message!");
this.latestMessages.setXMLMessage(xmlMessage);
break;
}
//RaceStartStatus.
case RACESTARTSTATUS: {
//System.out.println("Race Start Status Message");
break;
}
//YachtEventCode.
case YACHTEVENTCODE: {
//YachtEventCode yachtEventCode = (YachtEventCode) message;
//System.out.println("Yacht Event Code!");
//No decoder for this.
break;
}
//YachtActionCode.
case YACHTACTIONCODE: {
//YachtActionCode yachtActionCode = (YachtActionCode) message;
//System.out.println("Yacht Action Code!");
// No decoder for this.
break;
}
//ChatterText.
case CHATTERTEXT: {
//ChatterText chatterText = (ChatterText) message;
//System.out.println("Chatter Text Message!");
//No decoder for this.
break;
}
//BoatLocation.
case BOATLOCATION: {
BoatLocation boatLocation = (BoatLocation) message;
//System.out.println("Boat Location!");
BoatLocation existingBoatLocation = this.latestMessages.getBoatLocationMap().get(boatLocation.getSourceID());
if (existingBoatLocation != null) {
//If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
if (boatLocation.getTime() > existingBoatLocation.getTime()) {
//If it is, replace the old message.
this.latestMessages.setBoatLocation(boatLocation);
}
} else {
//If the map _doesn't_ already contain a message for this boat, insert the message.
this.latestMessages.setBoatLocation(boatLocation);
}
break;
}
//MarkRounding.
case MARKROUNDING: {
MarkRounding markRounding = (MarkRounding) message;
//System.out.println("Mark Rounding Message!");
MarkRounding existingMarkRounding = this.latestMessages.getMarkRoundingMap().get(markRounding.getSourceID());
if (existingMarkRounding != null) {
//If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
if (markRounding.getTime() > existingMarkRounding.getTime()) {
//If it is, replace the old message.
this.latestMessages.setMarkRounding(markRounding);
}
} else {
//If the map _doesn't_ already contain a message for this boat, insert the message.
this.latestMessages.setMarkRounding(markRounding);
}
break;
}
//CourseWinds.
case COURSEWIND: {
//System.out.println("Course Wind Message!");
CourseWinds courseWinds = (CourseWinds) message;
this.latestMessages.setCourseWinds(courseWinds);
break;
}
}
//Main loop.
// take message
// create command
// place in command queue
}
}
}
@@ -1,18 +1,15 @@
package visualiser.gameController;
import mock.exceptions.CommandConstructionException;
import mock.model.MockRace;
import mock.model.commandFactory.Command;
import mock.model.commandFactory.CommandFactory;
import mock.model.commandFactory.CompositeCommand;
import network.BinaryMessageDecoder;
import network.Exceptions.InvalidMessageException;
import network.MessageDecoders.BoatActionDecoder;
import network.Messages.AC35Data;
import network.Messages.BoatAction;
import network.Messages.Enums.MessageType;
import shared.model.RunnableWithFramePeriod;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -20,7 +17,7 @@ import java.util.logging.Logger;
/**
* Service for dispatching key press data to race from client
*/
public class ControllerServer implements Runnable {
public class ControllerServer implements RunnableWithFramePeriod {
/**
@@ -33,6 +30,11 @@ public class ControllerServer implements Runnable {
* Collection of commands from client for race to execute.
*/
private CompositeCommand compositeCommand;
/**
* The context for each command.
*/
private MockRace raceState;
/**
* This is the source ID associated with the client.
@@ -46,15 +48,18 @@ public class ControllerServer implements Runnable {
* @param compositeCommand Commands for the race to execute.
* @param inputQueue The queue of messages to read from.
* @param clientSourceID The source ID of the client's boat.
* @param raceState The context for each command.
*/
public ControllerServer(CompositeCommand compositeCommand, BlockingQueue<AC35Data> inputQueue, int clientSourceID) {
public ControllerServer(CompositeCommand compositeCommand, BlockingQueue<AC35Data> inputQueue, int clientSourceID, MockRace raceState) {
this.compositeCommand = compositeCommand;
this.inputQueue = inputQueue;
this.clientSourceID = clientSourceID;
this.raceState = raceState;
}
/**
* Wait for controller key input from client and loop.
*/
@@ -70,10 +75,17 @@ public class ControllerServer implements Runnable {
BoatAction boatAction = (BoatAction) message;
boatAction.setSourceID(clientSourceID);
Command command = CommandFactory.createCommand(boatAction);
compositeCommand.addCommand(command);
try {
Command command = CommandFactory.createCommand(raceState, boatAction);
compositeCommand.addCommand(command);
} catch (CommandConstructionException e) {
Logger.getGlobal().log(Level.WARNING, "ControllerServer could not create a Command for BoatAction: " + boatAction + ".", e);
}
}
@@ -81,6 +93,7 @@ public class ControllerServer implements Runnable {
Logger.getGlobal().log(Level.WARNING, "ControllerServer Interrupted while waiting for message on incoming message queue.", e);
Thread.currentThread().interrupt();
return;
}
}
@@ -22,16 +22,14 @@ public class Ping implements Runnable {
this.rc = rc;
}
public boolean pingPort(){
//TODO the connection needs to be moved to its own thread, so it doesn't block fx thread.
public boolean pingPort() {
InetSocketAddress i = new InetSocketAddress(hostname, port);
try (Socket s = new Socket()){
s.connect(i, 750);//TODO this should be at least a second or two, once moved to its own thread
s.connect(i, 1500);
s.shutdownInput();
s.shutdownOutput();
s.close();
rc.statusProperty().set("Ready");
//System.out.println(String.valueOf(s.isClosed()));
return true;
} catch (IOException e) {
rc.statusProperty().set("Offline");
@@ -314,6 +314,10 @@ public class ResizableRaceCanvas extends ResizableCanvas {
//The position may be null if we haven't received any BoatLocation messages yet.
if (boat.getCurrentPosition() != null) {
if (boat.isClientBoat()) {
drawClientBoat(boat);
}
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
@@ -343,6 +347,39 @@ public class ResizableRaceCanvas extends ResizableCanvas {
}
/**
* Draws extra decorations to show which boat has been assigned to the client.
* @param boat The client's boat.
*/
private void drawClientBoat(VisualiserBoat boat) {
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
//The x coordinates of each vertex of the boat.
double[] x = {
pos.getX() - 12,
pos.getX(),
pos.getX() + 12 };
//The y coordinates of each vertex of the boat.
double[] y = {
pos.getY() + 24,
pos.getY() - 24,
pos.getY() + 24 };
//The above shape is essentially a triangle 24px wide, and 48 long.
//Draw the boat.
gc.setFill(Color.BLACK);
gc.save();
rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
gc.fillPolygon(x, y, 3);
gc.restore();
}
/**
* Draws the wake for a given boat.
@@ -1,12 +1,7 @@
package visualiser.model;
import mock.app.MockOutput;
import mock.enums.ConnectionStateEnum;
import mock.exceptions.SourceIDAllocationException;
import mock.model.HeartBeatService;
import mock.model.SourceIdAllocator;
import mock.model.commandFactory.CompositeCommand;
import network.Messages.AC35Data;
import network.Messages.Enums.JoinAcceptanceEnum;
import network.Messages.Enums.MessageType;
@@ -19,7 +14,6 @@ import network.StreamRelated.MessageSerialiser;
import shared.exceptions.HandshakeException;
import visualiser.app.VisualiserInput;
import visualiser.gameController.ControllerClient;
import visualiser.gameController.ControllerServer;
import java.io.IOException;
import java.net.Socket;
@@ -38,6 +32,11 @@ public class ServerConnection implements Runnable {
*/
private Socket socket;
/**
* The source ID that has been allocated to the client.
*/
private int allocatedSourceID = 0;
/**
* Latest snapshot of the race, received from the server.
@@ -106,6 +105,9 @@ public class ServerConnection implements Runnable {
new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser).start();
new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser).start();
this.controllerClient = new ControllerClient(outputQueue);
}
@@ -135,25 +137,23 @@ public class ServerConnection implements Runnable {
connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE;
sendJoinAcceptanceMessage(RequestToJoinEnum.PARTICIPANT);
sendRequestToJoinMessage(RequestToJoinEnum.PARTICIPANT);
JoinAcceptance joinAcceptance = waitForJoinAcceptance();
int allocatedSourceID = 0;
//If we join successfully...
if (joinAcceptance.getAcceptanceType() == JoinAcceptanceEnum.JOIN_SUCCESSFUL) {
allocatedSourceID = joinAcceptance.getSourceID();
//TODO need to do something with the ID - maybe flag the correct visualiser boat as being the client's boat?
this.allocatedSourceID = joinAcceptance.getSourceID();
this.controllerClient = new ControllerClient(inputQueue);
//new Thread(controllerClient, "ServerConnection.run()->ControllerClient thread " + controllerClient).start();
}
this.visualiserInput = new VisualiserInput(latestMessages, outputQueue);
this.visualiserInput = new VisualiserInput(latestMessages, inputQueue);
new Thread(visualiserInput, "ServerConnection.run()->VisualiserInput thread " + visualiserInput).start();
@@ -200,7 +200,7 @@ public class ServerConnection implements Runnable {
* @param requestType The type of request to send
* @throws HandshakeException Thrown if the thread is interrupted while placing message on the outgoing message queue.
*/
private void sendJoinAcceptanceMessage(RequestToJoinEnum requestType) throws HandshakeException {
private void sendRequestToJoinMessage(RequestToJoinEnum requestType) throws HandshakeException {
//Send them the source ID.
RequestToJoin requestToJoin = new RequestToJoin(requestType);
@@ -217,12 +217,27 @@ public class ServerConnection implements Runnable {
/**
* Determines whether or not this connection is still alive.
* This is based off whether the {@link MessageSerialiser} is still alive.
* This is based off whether the {@link MessageDeserialiser} is still alive.
* @return True if it is alive, false otherwise.
*/
public boolean isAlive() {
return messageSerialiser.isRunning();
return messageDeserialiser.isRunning();
}
/**
* Returns the controller client, which writes BoatAction messages to the outgoing queue.
* @return The ControllerClient.
*/
public ControllerClient getControllerClient() {
return controllerClient;
}
/**
* Returns the source ID that has been allocated to the client.
* @return Source ID allocated to the client. 0 if it hasn't been allocated.
*/
public int getAllocatedSourceID() {
return allocatedSourceID;
}
}
@@ -57,6 +57,11 @@ public class VisualiserBoat extends Boat {
*/
private static final double wakeScale = 5;
/**
* If true then this boat has been allocated to the client.
*/
private boolean isClientBoat = false;
@@ -214,4 +219,21 @@ public class VisualiserBoat extends Boat {
return " -";
}
}
/**
* Returns whether or not this boat has been assigned to the client.
* @return True if this is the client's boat.
*/
public boolean isClientBoat() {
return isClientBoat;
}
/**
* Sets whether or not this boat has been assigned to the client.
* @param clientBoat True if this is the client's boat.
*/
public void setClientBoat(boolean clientBoat) {
isClientBoat = clientBoat;
}
}
@@ -347,13 +347,16 @@ public class VisualiserRace extends Race implements Runnable {
@Override
public void handle(long arg0) {
//Calculate the frame period.
long currentFrameTime = System.currentTimeMillis();
long framePeriod = currentFrameTime - lastFrameTime;
//Update race status.
updateRaceStatus(latestMessages.getRaceStatus());
if (latestMessages.getRaceStatus() != null) {
updateRaceStatus(latestMessages.getRaceStatus());
}
//Update racing boats.