From 7cc39abe5772bf29bc541535d0076eb7c8ce8a6b Mon Sep 17 00:00:00 2001 From: fjc40 Date: Thu, 10 Aug 2017 12:13:40 +1200 Subject: [PATCH 01/16] WIP. Probably need to cherry pick stuff out of here. Added ClientConnection and server-side handshake. Added MessageSerialiser and Deserialiser. #story[1095] --- .../java/mock/app/ConnectionAcceptor.java | 100 ++++-- .../src/main/java/mock/app/Event.java | 90 +++-- .../src/main/java/mock/app/MockOutput.java | 307 ++---------------- .../java/mock/enums/ConnectionStateEnum.java | 84 +++++ .../EventConstructionException.java | 24 ++ .../SourceIDAllocationException.java | 24 ++ .../java/mock/model/ClientConnection.java | 243 ++++++++++++++ .../java/mock/model/HeartBeatService.java | 110 +++++++ .../src/main/java/mock/model/MockRace.java | 20 ++ .../src/main/java/mock/model/RaceLogic.java | 39 ++- .../src/main/java/mock/model/RaceServer.java | 76 +++-- .../java/mock/model/SourceIdAllocator.java | 70 ++++ .../commandFactory/CompositeCommand.java | 25 ++ .../MessageControllers/MessageController.java | 9 + .../RaceVisionByteEncoder.java | 28 +- .../network/MessageRouters/MessageRouter.java | 11 + .../java/network/Messages/BoatAction.java | 21 ++ .../java/network/Messages/LatestMessages.java | 26 +- .../java/network/Messages/RaceSnapshot.java | 41 +++ .../StreamRelated/MessageDeserialiser.java | 156 +++++++++ .../StreamRelated/MessageSerialiser.java | 116 +++++++ .../exceptions/BoatNotFoundException.java | 15 + .../shared/exceptions/HandshakeException.java | 24 ++ .../shared/model/RunnableWithFramePeriod.java | 64 ++++ .../Controllers/ConnectionController.java | 29 +- .../Controllers/HostController.java | 16 +- .../Controllers/RaceController.java | 7 +- .../Controllers/StartController.java | 25 +- .../java/visualiser/app/VisualiserInput.java | 306 ++--------------- .../gameController/ControllerClient.java | 40 +-- .../gameController/ControllerServer.java | 81 +++-- .../gameController/Keys/KeyFactory.java | 4 +- .../visualiser/model/ServerConnection.java | 228 +++++++++++++ .../mock/model/SourceIdAllocatorTest.java | 126 +++++++ .../model/commandFactory/WindCommandTest.java | 31 ++ 35 files changed, 1830 insertions(+), 786 deletions(-) create mode 100644 racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java create mode 100644 racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java create mode 100644 racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java create mode 100644 racevisionGame/src/main/java/mock/model/ClientConnection.java create mode 100644 racevisionGame/src/main/java/mock/model/HeartBeatService.java create mode 100644 racevisionGame/src/main/java/mock/model/SourceIdAllocator.java create mode 100644 racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java create mode 100644 racevisionGame/src/main/java/network/MessageControllers/MessageController.java create mode 100644 racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java create mode 100644 racevisionGame/src/main/java/network/Messages/RaceSnapshot.java create mode 100644 racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java create mode 100644 racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java create mode 100644 racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java create mode 100644 racevisionGame/src/main/java/shared/exceptions/HandshakeException.java create mode 100644 racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java create mode 100644 racevisionGame/src/main/java/visualiser/model/ServerConnection.java create mode 100644 racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java create mode 100644 racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java diff --git a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java index 85548a45..120da194 100644 --- a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java +++ b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java @@ -1,21 +1,22 @@ package mock.app; +import mock.enums.ConnectionStateEnum; +import mock.model.ClientConnection; +import mock.model.SourceIdAllocator; +import mock.model.commandFactory.CompositeCommand; import network.Messages.Enums.XMLMessageType; import network.Messages.LatestMessages; +import network.Messages.RaceSnapshot; import network.Messages.XMLMessage; -import org.mockito.Mock; -import visualiser.gameController.ControllerServer; -import java.io.DataOutputStream; import java.io.IOException; -import java.lang.reflect.Array; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.ArrayBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Connection acceptor for multiple clients @@ -31,10 +32,31 @@ public class ConnectionAcceptor implements Runnable { * Socket used to listen for clients on. */ private ServerSocket serverSocket; - //mock outputs - private ArrayBlockingQueue mockOutputList = new ArrayBlockingQueue<>(16, true); - //latest messages + + + /** + * List of client connections. + */ + private ArrayBlockingQueue clientConnections = new ArrayBlockingQueue<>(16, true); + + /** + * Snapshot of the race. + */ private LatestMessages latestMessages; + + /** + * Collection of commands from clients for race to execute. + */ + private CompositeCommand compositeCommand; + + /** + * Used to allocate source IDs to clients. + */ + private SourceIdAllocator sourceIdAllocator; + + + + //Acknowledgement number for packets private int ackNumber = 0; //race xml sequence number @@ -47,14 +69,20 @@ public class ConnectionAcceptor implements Runnable { /** * Connection Acceptor Constructor * @param latestMessages Latest messages to be sent + * @param compositeCommand Collection of commands for race to execute. + * @param sourceIdAllocator Object used to allocate source IDs for clients. * @throws IOException if a server socket cannot be instantiated. */ - public ConnectionAcceptor(LatestMessages latestMessages) throws IOException { + public ConnectionAcceptor(LatestMessages latestMessages, CompositeCommand compositeCommand, SourceIdAllocator sourceIdAllocator) throws IOException { this.latestMessages = latestMessages; + this.compositeCommand = compositeCommand; + this.sourceIdAllocator = sourceIdAllocator; + this.serverSocket = new ServerSocket(serverPort); - CheckClientConnection checkClientConnection = new CheckClientConnection(mockOutputList); + CheckClientConnection checkClientConnection = new CheckClientConnection(clientConnections); new Thread(checkClientConnection, "ConnectionAcceptor()->CheckClientConnection thread").start(); + } public String getAddress() throws UnknownHostException { @@ -71,28 +99,26 @@ public class ConnectionAcceptor implements Runnable { @Override public void run() { - while(mockOutputList.remainingCapacity() > 0) { + while(clientConnections.remainingCapacity() > 0) { try { System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE Socket mockSocket = serverSocket.accept(); - //TODO at this point we need to assign the connection a boat source ID, if they requested to participate. - DataOutputStream outToVisualiser = new DataOutputStream(mockSocket.getOutputStream()); - MockOutput mockOutput = new MockOutput(latestMessages, outToVisualiser); - ControllerServer controllerServer = new ControllerServer(mockSocket); //TODO probably pass assigned boat source ID into ControllerServer. + ClientConnection clientConnection = new ClientConnection(mockSocket, sourceIdAllocator, latestMessages, compositeCommand); + + clientConnections.add(clientConnection); + + new Thread(clientConnection, "ConnectionAcceptor.run()->ClientConnection thread " + clientConnection).start(); - new Thread(mockOutput, "ConnectionAcceptor.run()->MockOutput thread" + mockOutput).start(); - new Thread(controllerServer, "ConnectionAcceptor.run()->ControllerServer thread" + controllerServer).start(); - mockOutputList.add(mockOutput); - System.out.println(String.format("%d number of Visualisers Connected.", mockOutputList.size()));//TEMP + Logger.getGlobal().log(Level.INFO, String.format("%d number of Visualisers Connected.", clientConnections.size())); } catch (IOException e) { - e.printStackTrace();//TODO handle this properly + Logger.getGlobal().log(Level.WARNING, "Got an IOException while a client was attempting to connect.", e); } @@ -104,14 +130,14 @@ public class ConnectionAcceptor implements Runnable { */ class CheckClientConnection implements Runnable{ - private ArrayBlockingQueue mocks; + private ArrayBlockingQueue connections; /** * Constructor - * @param mocks Mocks "connected" + * @param connections Clients "connected" */ - public CheckClientConnection(ArrayBlockingQueue mocks){ - this.mocks = mocks; + public CheckClientConnection(ArrayBlockingQueue connections){ + this.connections = connections; } /** @@ -119,21 +145,27 @@ public class ConnectionAcceptor implements Runnable { */ @Override public void run() { - double timeSinceLastHeartBeat = System.currentTimeMillis(); + while(true) { - //System.out.println(mocks.size());//used to see current amount of visualisers connected. - ArrayBlockingQueue m = new ArrayBlockingQueue<>(16, true, mocks); - for (MockOutput mo : m) { - try { - mo.sendHeartBeat(); - } catch (IOException e) { - mocks.remove(mo); + //System.out.println(connections.size());//used to see current amount of visualisers connected. + ArrayBlockingQueue clientConnections = new ArrayBlockingQueue<>(16, true, connections); + + for (ClientConnection client : clientConnections) { + if (!client.isAlive()) { + connections.remove(client); + + Logger.getGlobal().log(Level.WARNING, "CheckClientConnection is removing the dead connection: " + client); } } + try { Thread.sleep(100); + } catch (InterruptedException e) { - e.printStackTrace(); + Logger.getGlobal().log(Level.WARNING, "CheckClientConnection was interrupted while sleeping.", e); + Thread.currentThread().interrupt(); + return; + } } } diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index b98de4dc..7d7c940b 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -1,10 +1,14 @@ package mock.app; import mock.dataInput.PolarParser; +import mock.exceptions.EventConstructionException; import mock.model.MockRace; import mock.model.Polars; import mock.model.RaceLogic; +import mock.model.SourceIdAllocator; +import mock.model.commandFactory.CompositeCommand; import network.Messages.LatestMessages; +import network.Messages.RaceSnapshot; import shared.dataInput.*; import shared.enums.XMLFileType; import shared.exceptions.InvalidBoatDataException; @@ -19,14 +23,18 @@ import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.logging.Level; +import java.util.logging.Logger; /** * A Race Event, this holds all of the race's information as well as handling the connection to its clients. */ public class Event { - private static Event theEvent = new Event(); + /** + * Contents of the various xml files. + */ private String raceXML; private String regattaXML; private String boatXML; @@ -35,36 +43,75 @@ public class Event { private Polars boatPolars; + /** + * Data sources containing data from the xml files. + */ + RaceDataSource raceDataSource; + BoatDataSource boatDataSource; + RegattaDataSource regattaDataSource; + + private ConnectionAcceptor connectionAcceptor; private LatestMessages latestMessages; + private CompositeCommand compositeCommand; + + /** + * This is used to allocate source IDs. + */ + private SourceIdAllocator sourceIdAllocator; + + + + + /** * Constructs an event, using various XML files. + * @throws EventConstructionException Thrown if we cannot create an Event for any reason. */ - private Event() { + public Event() throws EventConstructionException { + + //Read XML files. try { this.raceXML = getRaceXMLAtCurrentTime(XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8)); this.boatXML = XMLReader.readXMLFileToString("mock/mockXML/boatsSinglePlayer.xml", StandardCharsets.UTF_8); this.regattaXML = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8); - this.xmlFileType = XMLFileType.Contents; - this.boatPolars = PolarParser.parse("mock/polars/acc_polars.csv"); + } catch (TransformerException | XMLReaderException e) { + throw new EventConstructionException("Could not read XML files.", e); + } + + this.xmlFileType = XMLFileType.Contents; + + this.boatPolars = PolarParser.parse("mock/polars/acc_polars.csv"); + + + //Parse the XML files into data sources. + try { + this.raceDataSource = new RaceXMLReader(this.raceXML, this.xmlFileType); + this.boatDataSource = new BoatXMLReader(this.boatXML, this.xmlFileType); + this.regattaDataSource = new RegattaXMLReader(this.regattaXML, this.xmlFileType); + + + } catch (XMLReaderException | InvalidRaceDataException | InvalidRegattaDataException | InvalidBoatDataException e) { + throw new EventConstructionException("Could not parse XML files.", e); - this.latestMessages = new LatestMessages(); - this.connectionAcceptor = new ConnectionAcceptor(latestMessages); } - catch (IOException e) { - e.printStackTrace(); - } catch (XMLReaderException e) { - e.printStackTrace(); - } catch (TransformerException e) { - e.printStackTrace(); + + //Create connection acceptor. + this.sourceIdAllocator = new SourceIdAllocator(raceDataSource.getParticipants()); + this.compositeCommand = new CompositeCommand(); + this.latestMessages = new LatestMessages(); + + try { + this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator); + + } catch (IOException e) { + throw new EventConstructionException("Could not create ConnectionAcceptor.", e); } } - public static Event getEvent() { - return theEvent; - } + public String getAddress() throws UnknownHostException { return connectionAcceptor.getAddress(); @@ -76,23 +123,16 @@ public class Event { /** * Sends the initial race data and then begins race simulation. - * @throws InvalidRaceDataException Thrown if the race xml file cannot be parsed. - * @throws XMLReaderException Thrown if any of the xml files cannot be parsed. - * @throws InvalidBoatDataException Thrown if the boat xml file cannot be parsed. - * @throws InvalidRegattaDataException Thrown if the regatta xml file cannot be parsed. */ - public void start() throws InvalidRaceDataException, XMLReaderException, InvalidBoatDataException, InvalidRegattaDataException { + public void start() { new Thread(connectionAcceptor, "Event.Start()->ConnectionAcceptor thread").start(); sendXMLs(); - //Parse the XML files into data sources. - RaceDataSource raceDataSource = new RaceXMLReader(this.raceXML, this.xmlFileType); - BoatDataSource boatDataSource = new BoatXMLReader(this.boatXML, this.xmlFileType); - RegattaDataSource regattaDataSource = new RegattaXMLReader(this.regattaXML, this.xmlFileType); + //Create and start race. - RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages); + RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages, this.compositeCommand); new Thread(newRace, "Event.Start()->RaceLogic thread").start(); } diff --git a/racevisionGame/src/main/java/mock/app/MockOutput.java b/racevisionGame/src/main/java/mock/app/MockOutput.java index 87bc9f95..9536507b 100644 --- a/racevisionGame/src/main/java/mock/app/MockOutput.java +++ b/racevisionGame/src/main/java/mock/app/MockOutput.java @@ -2,37 +2,25 @@ package mock.app; -import network.BinaryMessageEncoder; -import network.Exceptions.InvalidMessageException; -import network.MessageEncoders.RaceVisionByteEncoder; import network.Messages.*; -import network.Messages.Enums.MessageType; +import shared.model.RunnableWithFramePeriod; -import java.io.DataOutputStream; -import java.io.IOException; -import java.net.SocketException; +import java.util.List; +import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; /** * TCP server to send race information to connected clients. */ -public class MockOutput implements Runnable -{ - /** - * Timestamp of the last sent heartbeat message. - */ - private long lastHeartbeatTime; +public class MockOutput implements RunnableWithFramePeriod { + - /** - * Period for the heartbeat - that is, how often we send it. - */ - private double heartbeatPeriod = 5.0; /** - * Output stream which wraps around mockSocket outstream. + * A queue to send messages to client. */ - private DataOutputStream outToVisualiser; + private BlockingQueue outgoingMessages; /** @@ -43,187 +31,21 @@ public class MockOutput implements Runnable - /** - * Ack numbers used in messages. - */ - private int ackNumber = 1; - /** - * Sequence number for heartbeat messages. - */ - private int heartbeatSequenceNum = 1; /** * Ctor. - * @param latestMessages Latests Messages that the Mock is to send out - * @param outToVisualiser DataStream to output to Visualisers - * @throws IOException if server socket cannot be opened. + * @param latestMessages Latest Messages that the Mock is to send out + * @param outgoingMessages A queue to place outgoing messages on. */ - public MockOutput(LatestMessages latestMessages, DataOutputStream outToVisualiser) throws IOException { - - this.outToVisualiser = outToVisualiser; - - this.lastHeartbeatTime = System.currentTimeMillis(); - + public MockOutput(LatestMessages latestMessages, BlockingQueue outgoingMessages) { + this.outgoingMessages = outgoingMessages; this.latestMessages = latestMessages; - - } - - - /** - * Increments the ackNumber value, and returns it. - * @return Incremented ackNumber. - */ - private int getNextAckNumber(){ - this.ackNumber++; - - return this.ackNumber; - } - - - /** - * Calculates the time since last heartbeat message, in seconds. - * @return Time since last heartbeat message, in seconds. - */ - private double timeSinceHeartbeat() { - long now = System.currentTimeMillis(); - return (now - lastHeartbeatTime) / 1000.0; - } - - - /** - * Generates the next heartbeat message and returns it. Increments the heartbeat sequence number. - * @return The next heartbeat message. - */ - private HeartBeat createHeartbeatMessage() { - - //Create the heartbeat message. - HeartBeat heartBeat = new HeartBeat(this.heartbeatSequenceNum); - heartbeatSequenceNum++; - - return heartBeat; - } - - /** - * Serializes a heartbeat message into a packet to be sent, and returns the byte array. - * @param heartBeat The heartbeat message to serialize. - * @return Byte array containing the next heartbeat message. - * @throws InvalidMessageException Thrown if the message cannot be encoded. - */ - private byte[] parseHeartbeat(HeartBeat heartBeat) throws InvalidMessageException { - - //Serializes the heartbeat message. - byte[] heartbeatMessage = RaceVisionByteEncoder.encode(heartBeat); - - //Places the serialized message in a packet. - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( - MessageType.HEARTBEAT, - System.currentTimeMillis(), - getNextAckNumber(), - (short) heartbeatMessage.length, - heartbeatMessage ); - - return binaryMessageEncoder.getFullMessage(); - - } - - /** - * Encodes/serialises a XMLMessage message, and returns it. - * @param xmlMessage The XMLMessage message to serialise. - * @return The XMLMessage message in a serialised form. - * @throws InvalidMessageException Thrown if the message cannot be encoded. - */ - private synchronized byte[] parseXMLMessage(XMLMessage xmlMessage) throws InvalidMessageException { - - //Serialize the xml message. - byte[] encodedXML = RaceVisionByteEncoder.encode(xmlMessage); - - //Place the message in a packet. - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( - MessageType.XMLMESSAGE, - System.currentTimeMillis(), - xmlMessage.getAckNumber(), //We use the ack number from the xml message. - (short) encodedXML.length, - encodedXML ); - - - return binaryMessageEncoder.getFullMessage(); - } - /** - * Encodes/serialises a BoatLocation message, and returns it. - * @param boatLocation The BoatLocation message to serialise. - * @return The BoatLocation message in a serialised form. - * @throws InvalidMessageException If the message cannot be encoded. - */ - private synchronized byte[] parseBoatLocation(BoatLocation boatLocation) throws InvalidMessageException { - - - //Encodes the message. - byte[] encodedBoatLoc = RaceVisionByteEncoder.encode(boatLocation); - - //Encodes the full message with header. - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( - MessageType.BOATLOCATION, - System.currentTimeMillis(), - getNextAckNumber(), - (short) encodedBoatLoc.length, - encodedBoatLoc ); - - - return binaryMessageEncoder.getFullMessage(); - - } - - /** - * Encodes/serialises a RaceStatus message, and returns it. - * @param raceStatus The RaceStatus message to serialise. - * @return The RaceStatus message in a serialised form. - * @throws InvalidMessageException Thrown if the message cannot be encoded. - */ - private synchronized byte[] parseRaceStatus(RaceStatus raceStatus) throws InvalidMessageException { - - //Encodes the messages. - byte[] encodedRaceStatus = RaceVisionByteEncoder.encode(raceStatus); - - //Encodes the full message with header. - BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( - MessageType.RACESTATUS, - System.currentTimeMillis(), - getNextAckNumber(), - (short) encodedRaceStatus.length, - encodedRaceStatus ); - - - return binaryMessageEncoder.getFullMessage(); - - - } - - /** - * Sends a heartbeat - * @throws IOException if the socket is no longer open at both ends the heartbeat returns an error. - */ - public void sendHeartBeat() throws IOException { - //Sends a heartbeat every so often. - if (timeSinceHeartbeat() >= heartbeatPeriod) { - - HeartBeat heartBeat = createHeartbeatMessage(); - - try { - outToVisualiser.write(parseHeartbeat(heartBeat)); - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode HeartBeat: " + heartBeat, e); - } - - lastHeartbeatTime = System.currentTimeMillis(); - } - } - /** * Sending loop of the Server */ @@ -251,107 +73,40 @@ public class MockOutput implements Runnable long previousFrameTime = System.currentTimeMillis(); boolean sentXMLs = false; - try { - while (!Thread.interrupted()) { - try { - long currentFrameTime = System.currentTimeMillis(); + while (!Thread.interrupted()) { - //This is the time elapsed, in milliseconds, since the last server "frame". - long framePeriod = currentFrameTime - previousFrameTime; - - //We only attempt to send packets every X milliseconds. - long minimumFramePeriod = 16; - if (framePeriod >= minimumFramePeriod) { - - //Send XML messages. - if (!sentXMLs) { - //Serialise them. - - try { - byte[] raceXMLBlob = parseXMLMessage(latestMessages.getRaceXMLMessage()); - byte[] regattaXMLBlob = parseXMLMessage(latestMessages.getRegattaXMLMessage()); - byte[] boatsXMLBlob = parseXMLMessage(latestMessages.getBoatXMLMessage()); - - //Send them. - outToVisualiser.write(raceXMLBlob); - outToVisualiser.write(regattaXMLBlob); - outToVisualiser.write(boatsXMLBlob); - sentXMLs = true; - - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode XMLMessage: " + latestMessages.getRaceXMLMessage(), e); - continue; //Go to next iteration. - } - - } - - //Sends the RaceStatus message. - if (this.latestMessages.getRaceStatus() != null) { - - try { - byte[] raceStatusBlob = this.parseRaceStatus(this.latestMessages.getRaceStatus()); - - this.outToVisualiser.write(raceStatusBlob); - - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode RaceStatus: " + latestMessages.getRaceStatus(), e); - } - } - - //Send all of the BoatLocation messages. - for (int sourceID : this.latestMessages.getBoatLocationMap().keySet()) { - - //Get the message. - BoatLocation boatLocation = this.latestMessages.getBoatLocation(sourceID); - if (boatLocation != null) { - - - try { - //Encode. - byte[] boatLocationBlob = this.parseBoatLocation(boatLocation); - - //Write it. - this.outToVisualiser.write(boatLocationBlob); - - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode BoatLocation: " + boatLocation, e); - } - - - - } - } + try { - previousFrameTime = currentFrameTime; + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentFrameTime, 16); + previousFrameTime = currentFrameTime; - } else { - //Wait until the frame period will be large enough. - long timeToWait = minimumFramePeriod - framePeriod; + //Send XML messages. + if (!sentXMLs) { - try { - Thread.sleep(timeToWait); - } catch (InterruptedException e) { - //If we get interrupted, exit the function. - Logger.getGlobal().log(Level.WARNING, "MockOutput.run().sleep(framePeriod) was interrupted on thread: " + Thread.currentThread(), e); - //Re-set the interrupt flag. - Thread.currentThread().interrupt(); - return; - } + outgoingMessages.put(latestMessages.getRaceXMLMessage()); + outgoingMessages.put(latestMessages.getRegattaXMLMessage()); + outgoingMessages.put(latestMessages.getBoatXMLMessage()); - } + sentXMLs = true; + } - } catch (SocketException e) { - break; + List snapshot = latestMessages.getSnapshot(); + for (AC35Data message : snapshot) { + outgoingMessages.put(message); } + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.WARNING, "MockOutput.run() interrupted while putting message in queue.", e); + Thread.currentThread().interrupt(); + return; } - } catch (IOException e) { - e.printStackTrace(); } + } diff --git a/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java b/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java new file mode 100644 index 00000000..4d4961bb --- /dev/null +++ b/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java @@ -0,0 +1,84 @@ +package mock.enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * The states in which a connection to a client may have. + */ +public enum ConnectionStateEnum { + + UNKNOWN(0), + + /** + * We're waiting for the client to complete the joining handshake (see {@link network.Messages.RequestToJoin}. + */ + WAITING_FOR_HANDSHAKE(1), + + /** + * The client has completed the handshake, and is connected. + */ + CONNECTED(2), + + /** + * The client has timed out. + */ + TIMED_OUT(3); + + + + + private byte value; + + /** + * Ctor. Creates a ConnectionStateEnum from a given primitive integer value, cast to a byte. + * @param value Integer, which is cast to byte, to construct from. + */ + private ConnectionStateEnum(int value) { + this.value = (byte) value; + } + + /** + * Returns the primitive value of the enum. + * @return Primitive value of the enum. + */ + public byte getValue() { + return value; + } + + + /** + * Stores a mapping between Byte values and ConnectionStateEnum values. + */ + private static final Map byteToStatusMap = new HashMap<>(); + + + /* + Static initialization block. Initializes the byteToStatusMap. + */ + static { + for (ConnectionStateEnum type : ConnectionStateEnum.values()) { + ConnectionStateEnum.byteToStatusMap.put(type.value, type); + } + } + + + /** + * Returns the enumeration value which corresponds to a given byte value. + * @param connectionState Byte value to convert to a ConnectionStateEnum value. + * @return The ConnectionStateEnum value which corresponds to the given byte value. + */ + public static ConnectionStateEnum fromByte(byte connectionState) { + //Gets the corresponding MessageType from the map. + ConnectionStateEnum type = ConnectionStateEnum.byteToStatusMap.get(connectionState); + + if (type == null) { + //If the byte value wasn't found, return the UNKNOWN connectionState. + return ConnectionStateEnum.UNKNOWN; + } else { + //Otherwise, return the connectionState. + return type; + } + + } +} diff --git a/racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java b/racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java new file mode 100644 index 00000000..0f1d9b9f --- /dev/null +++ b/racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java @@ -0,0 +1,24 @@ +package mock.exceptions; + +/** + * An exception thrown when we cannot create an {@link mock.app.Event}. + */ +public class EventConstructionException extends Exception { + + /** + * Constructs the exception with a given message. + * @param message Message to store. + */ + public EventConstructionException(String message) { + super(message); + } + + /** + * Constructs the exception with a given message and cause. + * @param message Message to store. + * @param cause Cause to store. + */ + public EventConstructionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java b/racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java new file mode 100644 index 00000000..6623d9cb --- /dev/null +++ b/racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java @@ -0,0 +1,24 @@ +package mock.exceptions; + +/** + * An exception thrown when we cannot allocate a source ID. + */ +public class SourceIDAllocationException extends Exception { + + /** + * Constructs the exception with a given message. + * @param message Message to store. + */ + public SourceIDAllocationException(String message) { + super(message); + } + + /** + * Constructs the exception with a given message and cause. + * @param message Message to store. + * @param cause Cause to store. + */ + public SourceIDAllocationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/mock/model/ClientConnection.java b/racevisionGame/src/main/java/mock/model/ClientConnection.java new file mode 100644 index 00000000..71d85348 --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/ClientConnection.java @@ -0,0 +1,243 @@ +package mock.model; + + +import mock.app.MockOutput; +import mock.enums.ConnectionStateEnum; +import shared.exceptions.HandshakeException; +import mock.exceptions.SourceIDAllocationException; +import mock.model.commandFactory.CompositeCommand; +import network.Messages.*; +import network.Messages.Enums.JoinAcceptanceEnum; +import network.Messages.Enums.MessageType; +import network.Messages.Enums.RequestToJoinEnum; +import network.StreamRelated.MessageDeserialiser; +import network.StreamRelated.MessageSerialiser; +import visualiser.gameController.ControllerServer; + +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class handles the client connection handshake, and creation of MockOutput and ControllerServer. + */ +public class ClientConnection implements Runnable { + + /** + * The socket for the client's connection. + */ + private Socket socket; + + /** + * Periodically sends HeartBeat messages to client. + */ + private HeartBeatService heartBeatService; + + + /** + * Used to allocate source ID to client, if they request to participate. + */ + private SourceIdAllocator sourceIdAllocator; + + /** + * Latest snapshot of the race, to send to client. Currently only used for XML messages. + */ + private LatestMessages latestMessages; + + + /** + * Collection of commands from client for race to execute. + */ + private CompositeCommand compositeCommand; + + /** + * Used to send the race snapshot to client. + */ + private MockOutput mockOutput; + + /** + * Used to receive client input, and turn it into commands. + */ + private ControllerServer controllerServer; + + + /** + * Used to write messages to socket. + */ + private MessageSerialiser messageSerialiser; + + /** + * Stores messages to write to socket. + */ + private BlockingQueue outputQueue; + + /** + * Used to read messages from socket. + */ + private MessageDeserialiser messageDeserialiser; + + /** + * Stores messages read from socket. + */ + private BlockingQueue inputQueue; + + /** + * The state of the connection to the client. + */ + private ConnectionStateEnum connectionState = ConnectionStateEnum.UNKNOWN; + + + + + + + /** + * Creates a client connection, using a given socket. + * @param socket The socket which connects to the client. + * @param sourceIdAllocator Used to allocate a source ID for the client. + * @param latestMessages Latest race snapshot to send to client. + * @param compositeCommand Collection of commands for race to execute. + * @throws IOException Thrown if there is a problem with the client socket. + */ + public ClientConnection(Socket socket, SourceIdAllocator sourceIdAllocator, LatestMessages latestMessages, CompositeCommand compositeCommand) throws IOException { + this.socket = socket; + this.sourceIdAllocator = sourceIdAllocator; + this.latestMessages = latestMessages; + this.compositeCommand = compositeCommand; + + this.outputQueue = new LinkedBlockingQueue<>(); + this.inputQueue = new LinkedBlockingQueue<>(); + + + this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue); + this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue); + + new Thread(messageSerialiser, "ClientConnection()->MessageSerialiser thread " + messageSerialiser).start(); + new Thread(messageDeserialiser, "ClientConnection()->MessageDeserialiser thread " + messageDeserialiser).start(); + + + this.heartBeatService = new HeartBeatService(outputQueue); + new Thread(heartBeatService, "ClientConnection()->HeartBeatService thread " + heartBeatService).start(); + + } + + + + @Override + public void run() { + try { + handshake(); + + } catch (HandshakeException | SourceIDAllocationException e) { + Logger.getGlobal().log(Level.WARNING, "Client handshake failed.", e); + Thread.currentThread().interrupt(); + return; + } + + } + + + /** + * Initiates the handshake with the client. + * @throws HandshakeException Thrown if something goes wrong with the handshake. + * @throws SourceIDAllocationException Thrown if we cannot allocate a sourceID. + */ + private void handshake() throws SourceIDAllocationException, HandshakeException { + + //This function is a bit messy, and could probably be refactored a bit. + + connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE; + + + + RequestToJoin requestToJoin = waitForRequestToJoin(); + + int allocatedSourceID = 0; + + //If they want to participate, give them a source ID number. + if (requestToJoin.getRequestType() == RequestToJoinEnum.PARTICIPANT) { + + allocatedSourceID = sourceIdAllocator.allocateSourceID(); + + this.controllerServer = new ControllerServer(compositeCommand, inputQueue, allocatedSourceID); + new Thread(controllerServer, "ClientConnection.run()->ControllerServer thread" + controllerServer).start(); + + } + + this.mockOutput = new MockOutput(latestMessages, outputQueue); + new Thread(mockOutput, "ClientConnection.run()->MockOutput thread" + mockOutput).start(); + + sendJoinAcceptanceMessage(allocatedSourceID); + + connectionState = ConnectionStateEnum.CONNECTED; + + } + + + /** + * Waits until the client sends a {@link RequestToJoin} message, and returns it. + * @return The {@link RequestToJoin} message. + * @throws HandshakeException Thrown if we get interrupted while waiting. + */ + private RequestToJoin waitForRequestToJoin() throws HandshakeException { + + try { + + + while (connectionState == ConnectionStateEnum.WAITING_FOR_HANDSHAKE) { + + AC35Data message = inputQueue.take(); + + //We need to wait until they actually send a join request. + if (message.getType() == MessageType.REQUEST_TO_JOIN) { + return (RequestToJoin) message; + } + + } + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " was interrupted while waiting on the incoming message queue.", e); + + } + + + throw new HandshakeException("Handshake was cancelled. Connection state is now: " + connectionState); + + } + + + /** + * Sends the client a {@link JoinAcceptance} message, containing their assigned sourceID. + * @param sourceID The sourceID to assign to client. + * @throws HandshakeException Thrown if the thread is interrupted while placing message on the outgoing message queue. + */ + private void sendJoinAcceptanceMessage(int sourceID) throws HandshakeException { + + //Send them the source ID. + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID); + + try { + outputQueue.put(joinAcceptance); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " interrupted while placing JoinAcceptance message on outgoing message queue.", e); + } + + } + + + /** + * Determines whether or not this connection is still alive. + * This is based off whether the {@link MessageSerialiser} is still alive. + * @return True if it is alive, false otherwise. + */ + public boolean isAlive() { + return messageSerialiser.isRunning(); + } + + +} diff --git a/racevisionGame/src/main/java/mock/model/HeartBeatService.java b/racevisionGame/src/main/java/mock/model/HeartBeatService.java new file mode 100644 index 00000000..232eb9ad --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/HeartBeatService.java @@ -0,0 +1,110 @@ +package mock.model; + +import network.Messages.AC35Data; +import network.Messages.HeartBeat; +import shared.model.RunnableWithFramePeriod; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * This class is responsible for sending {@link HeartBeat} messages to queue. + */ +public class HeartBeatService implements RunnableWithFramePeriod { + + /** + * Timestamp of the last sent heartbeat message. + */ + private long lastHeartbeatTime; + + /** + * Period for the heartbeat - that is, how often we send it. Milliseconds. + */ + private long heartbeatPeriod = 5000; + + + /** + * The messages we're writing to the stream. + */ + private BlockingQueue messagesToSend; + + + + /** + * Sequence number for heartbeat messages. + */ + private int heartbeatSequenceNum = 1; + + + /** + * Constructs a new HeartBeatService to send heartBeat messages to a given outputStream. + * @param messagesToSend The queue to send heartBeat messages to. + */ + public HeartBeatService(BlockingQueue messagesToSend) { + this.messagesToSend = messagesToSend; + this.lastHeartbeatTime = System.currentTimeMillis(); + } + + + + + /** + * Increments the {@link #heartbeatSequenceNum} value, and returns it. + * @return Incremented heat beat number. + */ + private int getNextHeartBeatNumber(){ + this.heartbeatSequenceNum++; + + return this.heartbeatSequenceNum; + } + + + + /** + * Generates the next heartbeat message and returns it. Increments the heartbeat sequence number. + * @return The next heartbeat message. + */ + private HeartBeat createHeartbeatMessage() { + + HeartBeat heartBeat = new HeartBeat(getNextHeartBeatNumber()); + + return heartBeat; + } + + + /** + * Puts a HeartBeat message on the message queue. + * @throws InterruptedException Thrown if the thread is interrupted. + */ + private void sendHeartBeat() throws InterruptedException { + + HeartBeat heartBeat = createHeartbeatMessage(); + + messagesToSend.put(heartBeat); + } + + + + @Override + public void run() { + + while (!Thread.interrupted()) { + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(lastHeartbeatTime, currentFrameTime, heartbeatPeriod); + lastHeartbeatTime = currentFrameTime; + + try { + sendHeartBeat(); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.WARNING, "HeartBeatService: " + this + " sendHeartBeat() was interrupted on thread: " + Thread.currentThread(), e); + Thread.currentThread().interrupt(); + return; + + } + } + + } +} diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 9755099d..e389f474 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -7,6 +7,7 @@ import shared.dataInput.BoatDataSource; import shared.dataInput.RaceDataSource; import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RegattaDataSource; +import shared.exceptions.BoatNotFoundException; import shared.model.*; import shared.model.Bearing; @@ -411,6 +412,25 @@ public class MockRace extends Race { return boats; } + /** + * Returns a boat by sourceID. + * @param sourceID The source ID the boat. + * @return The boat. + * @throws BoatNotFoundException Thrown if there is not boat with the specified sourceID. + */ + public MockBoat getBoat(int sourceID) throws BoatNotFoundException { + + for (MockBoat boat : boats) { + + if (boat.getSourceID() == sourceID) { + return boat; + } + + } + + throw new BoatNotFoundException("Boat with sourceID: " + sourceID + " was not found."); + } + /** * Changes the wind direction randomly, while keeping it within [windLowerBound, windUpperBound]. */ diff --git a/racevisionGame/src/main/java/mock/model/RaceLogic.java b/racevisionGame/src/main/java/mock/model/RaceLogic.java index 9e810761..42bea8ef 100644 --- a/racevisionGame/src/main/java/mock/model/RaceLogic.java +++ b/racevisionGame/src/main/java/mock/model/RaceLogic.java @@ -1,12 +1,15 @@ package mock.model; import javafx.animation.AnimationTimer; +import mock.model.commandFactory.CompositeCommand; import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.RaceStatusEnum; import network.Messages.LatestMessages; -import shared.model.Race; + + public class RaceLogic implements Runnable { + /** * State of current race modified by this object */ @@ -16,14 +19,18 @@ public class RaceLogic implements Runnable { */ private RaceServer server; + private CompositeCommand commands; + /** * Initialises race loop with state and server message queue * @param race state of race to modify * @param messages to send to server + * @param compositeCommand Commands from clients to execute. */ - public RaceLogic(MockRace race, LatestMessages messages) { + public RaceLogic(MockRace race, LatestMessages messages, CompositeCommand compositeCommand) { this.race = race; this.server = new RaceServer(race, messages); + this.commands = compositeCommand; } /** @@ -56,17 +63,13 @@ public class RaceLogic implements Runnable { //Provide boat's with an estimated time at next mark until the race starts. race.setBoatsTimeNextMark(race.getRaceClock().getCurrentTime()); - //Parse the boat locations. - server.parseBoatLocations(); - - //Parse the marks. - server.parseMarks(); + //Parse the race snapshot. + server.parseSnapshot(); // Change wind direction race.changeWindDirection(); - //Parse the race status. - server.parseRaceStatus(); + if (race.getRaceStatusEnum() == RaceStatusEnum.STARTED) { @@ -109,6 +112,9 @@ public class RaceLogic implements Runnable { //Get the current time. currentTime = System.currentTimeMillis(); + //Execute commands from clients. + commands.execute(race); + //Update race time. race.updateRaceTime(currentTime); @@ -123,7 +129,6 @@ public class RaceLogic implements Runnable { //If it is still racing, update its position. if (boat.getStatus() == BoatStatusEnum.RACING) { - race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli()); } @@ -141,15 +146,8 @@ public class RaceLogic implements Runnable { // Change wind direction race.changeWindDirection(); - //Parse the boat locations. - server.parseBoatLocations(); - - //Parse the marks. - server.parseMarks(); - - //Parse the race status. - server.parseRaceStatus(); - + //Parse the race snapshot. + server.parseSnapshot(); //Update the last frame time. this.lastFrameTime = currentTime; @@ -165,7 +163,7 @@ public class RaceLogic implements Runnable { @Override public void handle(long now) { - server.parseRaceStatus(); + server.parseSnapshot(); if (iters > 500) { stop(); @@ -173,4 +171,5 @@ public class RaceLogic implements Runnable { iters++; } }; + } diff --git a/racevisionGame/src/main/java/mock/model/RaceServer.java b/racevisionGame/src/main/java/mock/model/RaceServer.java index d776b693..969a4c71 100644 --- a/racevisionGame/src/main/java/mock/model/RaceServer.java +++ b/racevisionGame/src/main/java/mock/model/RaceServer.java @@ -1,10 +1,7 @@ package mock.model; -import network.Messages.BoatLocation; -import network.Messages.BoatStatus; +import network.Messages.*; import network.Messages.Enums.BoatLocationDeviceEnum; -import network.Messages.LatestMessages; -import network.Messages.RaceStatus; import network.Utils.AC35UnitConverter; import shared.model.Bearing; import shared.model.CompoundMark; @@ -21,10 +18,6 @@ public class RaceServer { private MockRace race; private LatestMessages latestMessages; - /** - * The sequence number of the latest RaceStatus message sent or received. - */ - private int raceStatusSequenceNumber = 1; /** * The sequence number of the latest BoatLocation message sent or received. @@ -39,10 +32,31 @@ public class RaceServer { /** - * Parses an individual marker boat, and sends it to mockOutput. + * Parses the race to create a snapshot, and places it in latestMessages. + */ + public void parseSnapshot() { + + List snapshotMessages = new ArrayList<>(); + + //Parse the boat locations. + snapshotMessages.addAll(parseBoatLocations()); + + //Parse the marks. + snapshotMessages.addAll(parseMarks()); + + //Parse the race status. + snapshotMessages.add(parseRaceStatus()); + + latestMessages.setSnapshot(snapshotMessages); + } + + + /** + * Parses an individual marker boat, and returns it. * @param mark The marker boat to parse. + * @return The BoatLocation message. */ - private void parseIndividualMark(Mark mark) { + private BoatLocation parseIndividualMark(Mark mark) { //Create message. BoatLocation boatLocation = new BoatLocation( mark.getSourceID(), @@ -57,13 +71,17 @@ public class RaceServer { //Iterates the sequence number. this.boatLocationSequenceNumber++; - this.latestMessages.setBoatLocation(boatLocation); + return boatLocation; } /** - * Parse the compound marker boats through mock output. + * Parse the compound marker boats, and returns a list of BoatLocation messages. + * @return BoatLocation messages for each mark. */ - public void parseMarks() { + private List parseMarks() { + + List markLocations = new ArrayList<>(race.getCompoundMarks().size()); + for (CompoundMark compoundMark : race.getCompoundMarks()) { //Get the individual marks from the compound mark. @@ -72,31 +90,40 @@ public class RaceServer { //If they aren't null, parse them (some compound marks only have one mark). if (mark1 != null) { - this.parseIndividualMark(mark1); + markLocations.add(this.parseIndividualMark(mark1)); } if (mark2 != null) { - this.parseIndividualMark(mark2); + markLocations.add(this.parseIndividualMark(mark2)); } } + + return markLocations; } /** - * Parse the boats in the race, and send it to mockOutput. + * Parse the boats in the race, and returns all of their BoatLocation messages. + * @return List of BoatLocation messages, for each boat. */ - public void parseBoatLocations() { + private List parseBoatLocations() { + + List boatLocations = new ArrayList<>(race.getBoats().size()); + //Parse each boat. for (MockBoat boat : race.getBoats()) { - this.parseIndividualBoatLocation(boat); + boatLocations.add(this.parseIndividualBoatLocation(boat)); } + + return boatLocations; } /** - * Parses an individual boat, and sends it to mockOutput. + * Parses an individual boat, and returns it. * @param boat The boat to parse. + * @return The BoatLocation message. */ - private void parseIndividualBoatLocation(MockBoat boat) { + private BoatLocation parseIndividualBoatLocation(MockBoat boat) { BoatLocation boatLocation = new BoatLocation( boat.getSourceID(), @@ -111,16 +138,17 @@ public class RaceServer { //Iterates the sequence number. this.boatLocationSequenceNumber++; - this.latestMessages.setBoatLocation(boatLocation); + return boatLocation; } /** - * Parses the race status, and sends it to mockOutput. + * Parses the race status, and returns it. + * @return The race status message. */ - public void parseRaceStatus() { + private RaceStatus parseRaceStatus() { //A race status message contains a list of boat statuses. List boatStatuses = new ArrayList<>(); @@ -151,6 +179,6 @@ public class RaceServer { race.getRaceType(), boatStatuses); - this.latestMessages.setRaceStatus(raceStatus); + return raceStatus; } } diff --git a/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java b/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java new file mode 100644 index 00000000..3b62a8a7 --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java @@ -0,0 +1,70 @@ +package mock.model; + + +import mock.exceptions.SourceIDAllocationException; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class is responsible for allocating boat source IDs for use in a race, upon request. + */ +public class SourceIdAllocator { + + + /** + * This list contains all unallocated source IDs. + */ + List unallocatedIDs = new ArrayList<>(); + + + /** + * This list contains all allocated source IDs. + */ + List allocatedIDs = new ArrayList<>(); + + + /** + * Creates a source ID allocator, using the given list of unallocated source IDs. + * @param unallocatedIDs List of unallocated source IDs. + */ + public SourceIdAllocator(List unallocatedIDs) { + //We need to copy the list. + this.unallocatedIDs.addAll(unallocatedIDs); + } + + + /** + * Allocates a source ID for a boat. + * @return The allocated source ID. + * @throws SourceIDAllocationException Thrown if we cannot allocate any more source IDs. + */ + public synchronized int allocateSourceID() throws SourceIDAllocationException { + + if (!unallocatedIDs.isEmpty()) { + + int sourceID = unallocatedIDs.remove(0); + + allocatedIDs.add(sourceID); + + return sourceID; + + } else { + throw new SourceIDAllocationException("Could not allocate a source ID."); + + } + } + + + /** + * Returns a source ID to the source ID allocator, so that it can be reused. + * @param sourceID Source ID to return. + */ + public void returnSourceID(Integer sourceID) { + + //We remove an Integer, not an int, so that we remove by value not by index. + allocatedIDs.remove(sourceID); + + unallocatedIDs.add(sourceID); + } +} diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java new file mode 100644 index 00000000..ff09103d --- /dev/null +++ b/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java @@ -0,0 +1,25 @@ +package mock.model.commandFactory; + +import mock.model.MockRace; + +import java.util.Stack; + +/** + * Wraps multiple commands into a composite to execute queued commands during a frame. + */ +public class CompositeCommand implements Command { + private Stack commands; + + public CompositeCommand() { + this.commands = new Stack<>(); + } + + public void addCommand(Command command) { + commands.push(command); + } + + @Override + public void execute(MockRace race) { + while(!commands.isEmpty()) commands.pop().execute(race); + } +} diff --git a/racevisionGame/src/main/java/network/MessageControllers/MessageController.java b/racevisionGame/src/main/java/network/MessageControllers/MessageController.java new file mode 100644 index 00000000..7b6cca14 --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageControllers/MessageController.java @@ -0,0 +1,9 @@ +package network.MessageControllers; + + + +public class MessageController { + + + +} diff --git a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java index 54c10272..303b30db 100644 --- a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java +++ b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java @@ -1,6 +1,7 @@ package network.MessageEncoders; +import network.BinaryMessageEncoder; import network.Exceptions.InvalidMessageException; import network.Exceptions.InvalidMessageTypeException; import network.Messages.*; @@ -104,7 +105,7 @@ public class RaceVisionByteEncoder { /** - * Encodes a given message. + * Encodes a given message, to be placed inside a binary message (see {@link BinaryMessageEncoder}). * @param message Message to encode. * @return Encoded message. * @throws InvalidMessageException If the message cannot be encoded. @@ -126,4 +127,29 @@ public class RaceVisionByteEncoder { } + /** + * Encodes a given messages, using a given ackNumber, and returns a binary message ready to be sent over-the-wire. + * @param message The message to send. + * @param ackNumber The ackNumber of the message. + * @return A binary message ready to be transmitted. + * @throws InvalidMessageException Thrown if the message cannot be encoded. + */ + public static byte[] encodeBinaryMessage(AC35Data message, int ackNumber) throws InvalidMessageException { + + //Encodes the message. + byte[] encodedMessage = RaceVisionByteEncoder.encode(message); + + //Encodes the full message with header. + BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder( + message.getType(), + System.currentTimeMillis(), + ackNumber, + (short) encodedMessage.length, + encodedMessage ); + + + return binaryMessageEncoder.getFullMessage(); + } + + } diff --git a/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java new file mode 100644 index 00000000..4eaa6dce --- /dev/null +++ b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java @@ -0,0 +1,11 @@ +package network.MessageRouters; + + +/** + * This class routes {@link network.Messages.AC35Data} messages to an appropriate message controller. + */ +public class MessageRouter { + + + +} diff --git a/racevisionGame/src/main/java/network/Messages/BoatAction.java b/racevisionGame/src/main/java/network/Messages/BoatAction.java index fcc96aa8..93c6a310 100644 --- a/racevisionGame/src/main/java/network/Messages/BoatAction.java +++ b/racevisionGame/src/main/java/network/Messages/BoatAction.java @@ -13,6 +13,12 @@ public class BoatAction extends AC35Data { */ private BoatActionEnum boatAction; + + /** + * The source ID of the boat this action relates to. + */ + private int sourceID = 0; + /** * Constructs a BoatActon message with a given action. * @param boatAction Action to use. @@ -30,4 +36,19 @@ public class BoatAction extends AC35Data { return boatAction; } + /** + * Returns the boat source ID for this message. + * @return The source ID for this message. + */ + public int getSourceID() { + return sourceID; + } + + /** + * Sets the boat source ID for this message. + * @param sourceID The source for this message. + */ + public void setSourceID(int sourceID) { + this.sourceID = sourceID; + } } diff --git a/racevisionGame/src/main/java/network/Messages/LatestMessages.java b/racevisionGame/src/main/java/network/Messages/LatestMessages.java index f35fc52e..147f58e7 100644 --- a/racevisionGame/src/main/java/network/Messages/LatestMessages.java +++ b/racevisionGame/src/main/java/network/Messages/LatestMessages.java @@ -3,9 +3,7 @@ package network.Messages; import network.Messages.Enums.XMLMessageType; import shared.dataInput.RaceDataSource; -import java.util.HashMap; -import java.util.Map; -import java.util.Observable; +import java.util.*; /** * This class contains a set of the latest messages received (e.g., the latest RaceStatus, the latest BoatLocation for each boat, etc...). @@ -44,6 +42,12 @@ public class LatestMessages extends Observable { private CourseWinds courseWinds; + /** + * A list of messages containing a snapshot of the race. + */ + private List snapshot = new ArrayList<>(); + + /** * The latest race data XML message. */ @@ -69,6 +73,22 @@ public class LatestMessages extends Observable { } + /** + * Returns a copy of the race snapshot. + * @return Copy of the race snapshot. + */ + public List getSnapshot() { + return new ArrayList<>(snapshot); + } + + + /** + * Sets the snapshot of the race. + * @param snapshot New snapshot of race. + */ + public void setSnapshot(List snapshot) { + this.snapshot = snapshot; + } /** diff --git a/racevisionGame/src/main/java/network/Messages/RaceSnapshot.java b/racevisionGame/src/main/java/network/Messages/RaceSnapshot.java new file mode 100644 index 00000000..212c8dab --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/RaceSnapshot.java @@ -0,0 +1,41 @@ +package network.Messages; + + +import java.util.ArrayList; +import java.util.List; + + +/** + * Represents a snapshot of the race's state. + * Contains a list of {@link AC35Data} messages. + * Send a copy of each message to a connected client. + */ +public class RaceSnapshot { + + /** + * The contents of the snapshot. + */ + private List snapshot; + + + /** + * Constructs a snapshot using a given list of messages. + * @param snapshot Messages to use as snapshot. + */ + public RaceSnapshot(List snapshot) { + this.snapshot = snapshot; + } + + + /** + * Gets the contents of the snapshot. + * This is a shallow copy. + * @return Contents of the snapshot. + */ + public List getSnapshot() { + + List copy = new ArrayList<>(snapshot); + + return copy; + } +} diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java new file mode 100644 index 00000000..39cb0024 --- /dev/null +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java @@ -0,0 +1,156 @@ +package network.StreamRelated; + + +import network.BinaryMessageDecoder; +import network.Exceptions.InvalidMessageException; +import network.MessageEncoders.RaceVisionByteEncoder; +import network.Messages.AC35Data; +import shared.model.RunnableWithFramePeriod; + +import java.io.*; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static network.Utils.ByteConverter.bytesToShort; + +/** + * This class is responsible for converting data from an input stream into a queue of {@link AC35Data} messages. + */ +public class MessageDeserialiser implements RunnableWithFramePeriod { + + + /** + * The stream we're reading from. + */ + private DataInputStream inputStream; + + /** + * The messages we've read. + */ + private BlockingQueue messagesRead; + + + /** + * Ack numbers used in messages. + */ + private int ackNumber = 1; + + + /** + * Constructs a new MessageSerialiser to write a queue of messages to a given stream. + * @param inputStream The stream to write to. + * @param messagesRead The messages to send. + */ + public MessageDeserialiser(InputStream inputStream, BlockingQueue messagesRead) { + this.inputStream = new DataInputStream(inputStream); + this.messagesRead = messagesRead; + } + + + /** + * Increments the ackNumber value, and returns it. + * @return Incremented ackNumber. + */ + private int getNextAckNumber(){ + this.ackNumber++; + + return this.ackNumber; + } + + + + /** + * Reads and returns the next message as an array of bytes from the input stream. Use getNextMessage() to get the actual message object instead. + * @return Encoded binary message bytes. + * @throws IOException Thrown when an error occurs while reading from the input stream. + */ + private byte[] getNextMessageBytes() throws IOException { + inputStream.mark(0); + short CRCLength = 4; + short headerLength = 15; + + //Read the header of the next message. + byte[] headerBytes = new byte[headerLength]; + inputStream.readFully(headerBytes); + + //Read the message body length. + byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength); + short messageBodyLength = bytesToShort(messageBodyLengthBytes); + + //Read the message body. + byte[] messageBodyBytes = new byte[messageBodyLength]; + inputStream.readFully(messageBodyBytes); + + //Read the message CRC. + byte[] messageCRCBytes = new byte[CRCLength]; + inputStream.readFully(messageCRCBytes); + + //Put the head + body + crc into one large array. + ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length); + messageBytes.put(headerBytes); + messageBytes.put(messageBodyBytes); + messageBytes.put(messageCRCBytes); + + return messageBytes.array(); + } + + + /** + * Reads and returns the next message object from the input stream. + * @return The message object. + * @throws IOException Thrown when an error occurs while reading from the input stream. + * @throws InvalidMessageException Thrown when the message is invalid in some way. + */ + private AC35Data getNextMessage() throws IOException, InvalidMessageException + { + //Get the next message from the socket as a block of bytes. + byte[] messageBytes = this.getNextMessageBytes(); + + //Decode the binary message into an appropriate message object. + BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes); + + return decoder.decode(); + + } + + + + @Override + public void run() { + + long previousFrameTime = System.currentTimeMillis(); + + while (!Thread.interrupted()) { + + + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentFrameTime, 16); + previousFrameTime = currentFrameTime; + + + //Reads the next message. + try { + AC35Data message = this.getNextMessage(); + messagesRead.add(message); + } + catch (InvalidMessageException | IOException e) { + + Logger.getGlobal().log(Level.WARNING, "Unable to read message.", e); + + try { + inputStream.reset(); + } catch (IOException e1) { + Logger.getGlobal().log(Level.WARNING, "Unable to reset inputStream.", e); + } + + } + + } + + } +} diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java new file mode 100644 index 00000000..02e6f7a6 --- /dev/null +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java @@ -0,0 +1,116 @@ +package network.StreamRelated; + + +import network.Exceptions.InvalidMessageException; +import network.MessageEncoders.RaceVisionByteEncoder; +import network.Messages.AC35Data; +import shared.model.RunnableWithFramePeriod; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class is responsible for writing a queue of {@link network.Messages.AC35Data} messages to an output stream. + */ +public class MessageSerialiser implements RunnableWithFramePeriod { + + + /** + * The stream we're writing to. + */ + private DataOutputStream outputStream; + + /** + * The messages we're writing to the stream. + */ + private BlockingQueue messagesToSend; + + + /** + * Ack numbers used in messages. + */ + private int ackNumber = 1; + + /** + * Determines whether or not this runnable is currently running. + */ + private boolean isRunning; + + + + /** + * Constructs a new MessageSerialiser to write a queue of messages to a given stream. + * @param outputStream The stream to write to. + * @param messagesToSend The messages to send. + */ + public MessageSerialiser(OutputStream outputStream, BlockingQueue messagesToSend) { + this.outputStream = new DataOutputStream(outputStream); + this.messagesToSend = messagesToSend; + } + + + /** + * Increments the ackNumber value, and returns it. + * @return Incremented ackNumber. + */ + private int getNextAckNumber(){ + this.ackNumber++; + + return this.ackNumber; + } + + /** + * Determines whether or not this runnable is running. + * @return True means that it is still running, false means that it has stopped. + */ + public boolean isRunning() { + return isRunning; + } + + + @Override + public void run() { + + long previousFrameTime = System.currentTimeMillis(); + + isRunning = true; + + while (isRunning) { + + + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentFrameTime, 16); + previousFrameTime = currentFrameTime; + + + //Send the messages. + List messages = new ArrayList<>(); + messagesToSend.drainTo(messages); + + for (AC35Data message : messages) { + try { + byte[] messageBytes = RaceVisionByteEncoder.encodeBinaryMessage(message, getNextAckNumber()); + + outputStream.write(messageBytes); + + + } catch (InvalidMessageException e) { + Logger.getGlobal().log(Level.WARNING, "Could not encode message: " + message, e); + + } catch (IOException e) { + Logger.getGlobal().log(Level.WARNING, "Could not write message to outputStream: " + outputStream, e); + isRunning = false; + + } + } + + } + + } +} diff --git a/racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java b/racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java new file mode 100644 index 00000000..f3fed55c --- /dev/null +++ b/racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java @@ -0,0 +1,15 @@ +package shared.exceptions; + +/** + * An exception thrown when a specific boat cannot be found. + */ +public class BoatNotFoundException extends Exception { + + public BoatNotFoundException(String message) { + super(message); + } + + public BoatNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/shared/exceptions/HandshakeException.java b/racevisionGame/src/main/java/shared/exceptions/HandshakeException.java new file mode 100644 index 00000000..2f62e286 --- /dev/null +++ b/racevisionGame/src/main/java/shared/exceptions/HandshakeException.java @@ -0,0 +1,24 @@ +package shared.exceptions; + +/** + * An exception thrown when we the client-server handshake fails. + */ +public class HandshakeException extends Exception { + + /** + * Constructs the exception with a given message. + * @param message Message to store. + */ + public HandshakeException(String message) { + super(message); + } + + /** + * Constructs the exception with a given message and cause. + * @param message Message to store. + * @param cause Cause to store. + */ + public HandshakeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java new file mode 100644 index 00000000..af633af3 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java @@ -0,0 +1,64 @@ +package shared.model; + + +import network.Exceptions.InvalidMessageException; +import network.MessageEncoders.RaceVisionByteEncoder; +import network.Messages.AC35Data; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This interface is a {@link Runnable} interface, with the ability to sleep until a given time period has elapsed. + */ +public interface RunnableWithFramePeriod extends Runnable { + + + + + + + /** + * Waits for enough time for the period of this frame to be greater than minimumFramePeriod. + * @param previousFrameTime The timestamp of the previous frame. + * @param currentFrameTime The timestamp of the current frame. + * @param minimumFramePeriod The minimum period the frame must be. + */ + default void waitForFramePeriod(long previousFrameTime, long currentFrameTime, long minimumFramePeriod) { + + + //This is the time elapsed, in milliseconds, since the last server "frame". + long framePeriod = currentFrameTime - previousFrameTime; + + //We only attempt to send packets every X milliseconds. + if (framePeriod >= minimumFramePeriod) { + return; + + } else { + //Wait until the frame period will be large enough. + long timeToWait = minimumFramePeriod - framePeriod; + + try { + Thread.sleep(timeToWait); + + } catch (InterruptedException e) { + //If we get interrupted, exit the function. + Logger.getGlobal().log(Level.SEVERE, "RunnableWithFramePeriod.waitForFramePeriod().sleep(framePeriod) was interrupted on thread: " + Thread.currentThread(), e); + //Re-set the interrupt flag. + Thread.currentThread().interrupt(); + return; + + } + + } + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java index ae8c682c..5f1e2d8d 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java @@ -144,32 +144,5 @@ public class ConnectionController extends Controller { } } - /** - * Sets up a new host - */ - public void addLocal() { - try { - //We don't want to host more than one game. - if (!currentlyHostingGame) { - Event game = Event.getEvent(); - urlField.textProperty().set(game.getAddress()); - portField.textProperty().set(Integer.toString(game.getPort())); - - game.start(); - addConnection(); - - currentlyHostingGame = true; - } - } catch (InvalidRaceDataException e) { - e.printStackTrace(); - } catch (XMLReaderException e) { - e.printStackTrace(); - } catch (InvalidBoatDataException e) { - e.printStackTrace(); - } catch (InvalidRegattaDataException e) { - e.printStackTrace(); - } catch (UnknownHostException e) { - e.printStackTrace(); - } - } + } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java index e87ea689..7873e8e6 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java @@ -6,6 +6,7 @@ import javafx.scene.control.*; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; import mock.app.Event; +import mock.exceptions.EventConstructionException; import shared.exceptions.InvalidBoatDataException; import shared.exceptions.InvalidRaceDataException; import shared.exceptions.InvalidRegattaDataException; @@ -17,6 +18,8 @@ import java.net.Socket; import java.net.URL; import java.net.UnknownHostException; import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Controller for Hosting a game. @@ -44,17 +47,12 @@ public class HostController extends Controller { */ public void hostGamePressed() throws IOException{ try { - Event game = Event.getEvent(); + Event game = new Event(); game.start(); connectSocket("localhost", 4942); - } catch (InvalidRaceDataException e) { - e.printStackTrace(); - } catch (XMLReaderException e) { - e.printStackTrace(); - } catch (InvalidBoatDataException e) { - e.printStackTrace(); - } catch (InvalidRegattaDataException e) { - e.printStackTrace(); + } catch (EventConstructionException e) { + Logger.getGlobal().log(Level.SEVERE, "Could not create Event.", e); + throw new RuntimeException(e); } } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index f34c57a8..5a1836f6 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -26,6 +26,8 @@ import visualiser.model.*; import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Controller used to display a running race. @@ -115,8 +117,9 @@ public class RaceController extends Controller { controllerClient.sendKey(controlKey); controlKey.onAction(); // Change key state if applicable event.consume(); - } catch (IOException e) { - e.printStackTrace(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Logger.getGlobal().log(Level.WARNING, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e); } } }); diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index 8db4ec60..2f13aae2 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -20,6 +20,7 @@ import shared.exceptions.InvalidRegattaDataException; import shared.exceptions.XMLReaderException; import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; +import visualiser.model.ServerConnection; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRace; @@ -27,6 +28,8 @@ import java.io.IOException; import java.net.Socket; import java.net.URL; import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Controller to for waiting for the race to start. @@ -66,18 +69,18 @@ public class StartController extends Controller implements Observer { @FXML private Label raceStatusLabel; - /** - * The object used to read packets from the connected server. + * Our connection to the server. */ - private VisualiserInput visualiserInput; + private ServerConnection serverConnection; + /** * The race object which describes the currently occurring race. */ private VisualiserRace visualiserRace; - private ControllerClient controllerClient; + /** * An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor. @@ -309,17 +312,17 @@ public class StartController extends Controller implements Observer { public void enterLobby(Socket socket) { startWrapper.setVisible(true); try { - //Begin reading packets from the socket/server. - this.visualiserInput = new VisualiserInput(socket); - //Send controller input to server - this.controllerClient = new ControllerClient(socket); + + LatestMessages latestMessages = new LatestMessages(); + this.serverConnection = new ServerConnection(socket, latestMessages); + + //Store a reference to latestMessages so that we can observe it. - LatestMessages latestMessages = this.visualiserInput.getLatestMessages(); latestMessages.addObserver(this); - new Thread(this.visualiserInput).start(); + new Thread(this.serverConnection).start(); } catch (IOException e) { - e.printStackTrace(); + Logger.getGlobal().log(Level.WARNING, "Could not connection to server.", e); } } diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java index e77a2fef..8ad14a1b 100644 --- a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java +++ b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java @@ -2,12 +2,14 @@ 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 static network.Utils.ByteConverter.bytesToShort; @@ -15,7 +17,7 @@ import static network.Utils.ByteConverter.bytesToShort; * TCP client which receives packets/messages from a race data source * (e.g., mock source, official source), and exposes them to any observers. */ -public class VisualiserInput implements Runnable { +public class VisualiserInput implements RunnableWithFramePeriod { /** * Timestamp of the last heartbeat. @@ -27,40 +29,28 @@ public class VisualiserInput implements Runnable { private long lastHeartbeatSequenceNum = -1; - /** - * The socket that we have connected to. - */ - private Socket connectionSocket; - /** - * InputStream (from the socket). + * Incoming messages from server. */ - private DataInputStream inStream; + private BlockingQueue incomingMessages; /** * An object containing the set of latest messages to write to. - * Every server frame, VisualiserInput reads messages from its inputStream, and write them to this. + * Every server frame, VisualiserInput reads messages from its incomingMessages, and write them to this. */ private LatestMessages latestMessages; - /** - * Ctor. - * @param socket Socket from which we will receive race data. - * @throws IOException If there is something wrong with the socket's input stream. + * Constructs a visualiserInput to convert an incoming stream of messages into LatestMessages. + * @param latestMessages Object to place messages in. + * @param incomingMessages The incoming queue of messages. */ - public VisualiserInput(Socket socket) throws IOException { - - this.connectionSocket = socket; - - //We wrap a DataInputStream around the socket's InputStream because it has the stream.readFully(buffer) function, which is a blocking read until the buffer has been filled. - this.inStream = new DataInputStream(connectionSocket.getInputStream()); - - this.latestMessages = new LatestMessages(); - + public VisualiserInput(LatestMessages latestMessages, BlockingQueue incomingMessages) { + this.latestMessages = latestMessages; + this.incomingMessages = incomingMessages; this.lastHeartbeatTime = System.currentTimeMillis(); } @@ -85,279 +75,21 @@ public class VisualiserInput implements Runnable { - /** - * Reads and returns the next message as an array of bytes from the socket. Use getNextMessage() to get the actual message object instead. - * @return Encoded binary message bytes. - * @throws IOException Thrown when an error occurs while reading from the socket. - */ - private byte[] getNextMessageBytes() throws IOException { - inStream.mark(0); - short CRCLength = 4; - short headerLength = 15; - - //Read the header of the next message. - byte[] headerBytes = new byte[headerLength]; - inStream.readFully(headerBytes); - - //Read the message body length. - byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength); - short messageBodyLength = bytesToShort(messageBodyLengthBytes); - - //Read the message body. - byte[] messageBodyBytes = new byte[messageBodyLength]; - inStream.readFully(messageBodyBytes); - - //Read the message CRC. - byte[] messageCRCBytes = new byte[CRCLength]; - inStream.readFully(messageCRCBytes); - - //Put the head + body + crc into one large array. - ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length); - messageBytes.put(headerBytes); - messageBytes.put(messageBodyBytes); - messageBytes.put(messageCRCBytes); - - return messageBytes.array(); - } - - /** - * Reads and returns the next message object from the socket. - * @return The message object. Use instanceof for concrete type. - * @throws IOException Thrown when an error occurs while reading from the socket. - * @throws InvalidMessageException Thrown when the message is invalid in some way. - */ - private AC35Data getNextMessage() throws IOException, InvalidMessageException - { - //Get the next message from the socket as a block of bytes. - byte[] messageBytes = this.getNextMessageBytes(); - - //Decode the binary message into an appropriate message object. - BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes); - - return decoder.decode(); - - } - - /** - * Main loop which reads messages from the socket, and exposes them. - */ - public void run(){ - boolean receiverLoop = true; - //receiver loop that gets the input - while (receiverLoop) { - - //If no heartbeat has been received in more the heartbeat period - //then the connection will need to be restarted. - //System.out.println("time since last heartbeat: " + timeSinceHeartbeat());//TEMP REMOVE - long heartBeatPeriod = 10 * 1000; - if (timeSinceHeartbeat() > heartBeatPeriod) { - System.out.println("Connection has stopped, trying to reconnect."); - - //Attempt to reconnect the socket. - try {//This attempt doesn't really work. Under what circumstances would - this.connectionSocket = new Socket(this.connectionSocket.getInetAddress(), this.connectionSocket.getPort()); - //this.connectionSocket.connect(this.connectionSocket.getRemoteSocketAddress()); - //Reset the heartbeat timer. - this.lastHeartbeatTime = System.currentTimeMillis(); - } - catch (IOException e) { - System.err.println("Unable to reconnect."); - - //Wait 500ms. Ugly hack, should refactor. - long waitPeriod = 500; - long waitTimeStart = System.currentTimeMillis() + waitPeriod; - - while (System.currentTimeMillis() < waitTimeStart){ - //Nothing. Busyloop. - } - - //Swallow the exception. - continue; - } - - } - - //Reads the next message. - AC35Data message; - try { - message = this.getNextMessage(); - } - catch (InvalidMessageException | IOException e) { - //Prints exception to stderr, and iterate loop (that is, read the next message). - System.err.println("Unable to read message: " + e.getMessage()); - try { - inStream.reset(); - } catch (IOException e1) { - e1.printStackTrace(); - } - //Continue to the next loop iteration/message. - continue; - } - - - //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; - } + @Override + public void run() { - //AverageWind. - case AVGWIND: { + //Handshake. - //System.out.println("Average Wind Message!"); - AverageWind averageWind = (AverageWind) message; + //Main loop. + // take message + // create command + // place in command queue - this.latestMessages.setAverageWind(averageWind); - break; - } - //Unrecognised message. - default: { - System.out.println("Broken Message!"); - break; - } - } - } } } diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java index bb91f2a4..9b38a5ca 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java +++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java @@ -3,6 +3,7 @@ package visualiser.gameController; import network.BinaryMessageEncoder; import network.Exceptions.InvalidMessageException; import network.MessageEncoders.RaceVisionByteEncoder; +import network.Messages.AC35Data; import network.Messages.BoatAction; import network.Messages.Enums.BoatActionEnum; import network.Messages.Enums.MessageType; @@ -13,6 +14,7 @@ import java.io.IOException; import java.net.Socket; import java.net.SocketException; import java.nio.ByteBuffer; +import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; @@ -20,28 +22,18 @@ import java.util.logging.Logger; * Basic service for sending key presses to game server */ public class ControllerClient { - /** - * Socket to server - */ - Socket socket; /** - * Output stream wrapper for socket to server + * Queue of messages to be sent to server. */ - DataOutputStream outputStream; + private BlockingQueue outgoingMessages; /** * Initialise controller client with live socket. - * @param socket to server + * @param outgoingMessages Queue to place messages on to send to server. */ - public ControllerClient(Socket socket) { - this.socket = socket; - - try { - this.outputStream = new DataOutputStream(socket.getOutputStream()); - } catch (IOException e) { - e.printStackTrace(); - } + public ControllerClient(BlockingQueue outgoingMessages) { + this.outgoingMessages = outgoingMessages; } /** @@ -49,27 +41,13 @@ public class ControllerClient { * @param key to send * @throws IOException if socket write fails */ - public void sendKey(ControlKey key) throws IOException { + public void sendKey(ControlKey key) throws InterruptedException { BoatActionEnum protocolCode = key.getProtocolCode(); if(protocolCode != BoatActionEnum.NOT_A_STATUS) { BoatAction boatAction = new BoatAction(protocolCode); - //Encode BoatAction. - try { - byte[] encodedBoatAction = RaceVisionByteEncoder.encode(boatAction); - - BinaryMessageEncoder binaryMessage = new BinaryMessageEncoder(MessageType.BOATACTION, System.currentTimeMillis(), 0, - (short) encodedBoatAction.length, encodedBoatAction); - - System.out.println("Sending out key: " + protocolCode); - outputStream.write(binaryMessage.getFullMessage()); - - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not encode BoatAction: " + boatAction, e); - - } - + outgoingMessages.put(boatAction); } } diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java index d4c62d11..3757dc01 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java +++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java @@ -1,16 +1,19 @@ package visualiser.gameController; +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.BoatActionEnum; -import visualiser.gameController.Keys.ControlKey; -import visualiser.gameController.Keys.KeyFactory; +import network.Messages.Enums.MessageType; import java.io.DataInputStream; import java.io.IOException; -import java.net.Socket; +import java.io.InputStream; +import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; @@ -18,57 +21,69 @@ import java.util.logging.Logger; * Service for dispatching key press data to race from client */ public class ControllerServer implements Runnable { + + /** - * Socket to client + * Queue of incoming messages from client. */ - private Socket socket; + private BlockingQueue inputQueue; + + /** - * Wrapper for input from client + * Collection of commands from client for race to execute. */ - private DataInputStream inputStream; + private CompositeCommand compositeCommand; /** - * Initialise server-side controller with live client socket - * @param socket to client + * This is the source ID associated with the client. */ - public ControllerServer(Socket socket) { - this.socket = socket; - try { - this.inputStream = new DataInputStream(this.socket.getInputStream()); - } catch (IOException e) { - e.printStackTrace(); - } + private int clientSourceID; + + + + /** + * Initialise server-side controller with live client socket. + * @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. + */ + public ControllerServer(CompositeCommand compositeCommand, BlockingQueue inputQueue, int clientSourceID) { + this.compositeCommand = compositeCommand; + this.inputQueue = inputQueue; + this.clientSourceID = clientSourceID; } + + /** * Wait for controller key input from client and loop. */ @Override public void run() { - while(true) { - byte[] message = new byte[20]; - try { - if (inputStream.available() > 0) { + while(!Thread.interrupted()) { - inputStream.read(message); + try { - BinaryMessageDecoder encodedMessage = new BinaryMessageDecoder(message); - BoatActionDecoder boatActionDecoder = new BoatActionDecoder(); + AC35Data message = inputQueue.take(); - try { - boatActionDecoder.decode(encodedMessage.getMessageBody()); - BoatAction boatAction = boatActionDecoder.getMessage(); - System.out.println("Received key: " + boatAction.getBoatAction()); + if (message.getType() == MessageType.BOATACTION) { - } catch (InvalidMessageException e) { - Logger.getGlobal().log(Level.WARNING, "Could not decode BoatAction message.", e); - } + BoatAction boatAction = (BoatAction) message; + boatAction.setSourceID(clientSourceID); + Command command = CommandFactory.createCommand(boatAction); + compositeCommand.addCommand(command); } - } catch (IOException e) { - e.printStackTrace(); + + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.WARNING, "ControllerServer Interrupted while waiting for message on incoming message queue.", e); + Thread.currentThread().interrupt(); + return; } + } + } } diff --git a/racevisionGame/src/main/java/visualiser/gameController/Keys/KeyFactory.java b/racevisionGame/src/main/java/visualiser/gameController/Keys/KeyFactory.java index ef1368f0..be95abd3 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/Keys/KeyFactory.java +++ b/racevisionGame/src/main/java/visualiser/gameController/Keys/KeyFactory.java @@ -27,8 +27,8 @@ public class KeyFactory { keyState.put("SPACE", new VMGKey("VMG")); keyState.put("SHIFT", new SailsToggleKey("Toggle Sails")); keyState.put("ENTER", new TackGybeKey("Tack/Gybe")); - keyState.put("PAGE_UP", new UpWindKey("Upwind")); - keyState.put("PAGE_DOWN", new DownWindKey("Downwind")); + keyState.put("UP", new UpWindKey("Upwind")); + keyState.put("DOWN", new DownWindKey("Downwind")); } /** diff --git a/racevisionGame/src/main/java/visualiser/model/ServerConnection.java b/racevisionGame/src/main/java/visualiser/model/ServerConnection.java new file mode 100644 index 00000000..ececdd4c --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/ServerConnection.java @@ -0,0 +1,228 @@ +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; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import network.Messages.LatestMessages; +import network.Messages.RequestToJoin; +import network.StreamRelated.MessageDeserialiser; +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; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class handles the client-server connection handshake, and creation of VisualiserInput and ControllerClient. + */ +public class ServerConnection implements Runnable { + + /** + * The socket for the connection to server. + */ + private Socket socket; + + + /** + * Latest snapshot of the race, received from the server. + */ + private LatestMessages latestMessages; + + + /** + * Used to convert incoming messages into a race snapshot. + */ + private VisualiserInput visualiserInput; + + /** + * Used to send client input to server. + */ + private ControllerClient controllerClient; + + + /** + * Used to write messages to socket. + */ + private MessageSerialiser messageSerialiser; + + /** + * Stores messages to write to socket. + */ + private BlockingQueue outputQueue; + + /** + * Used to read messages from socket. + */ + private MessageDeserialiser messageDeserialiser; + + /** + * Stores messages read from socket. + */ + private BlockingQueue inputQueue; + + /** + * The state of the connection to the client. + */ + private ConnectionStateEnum connectionState = ConnectionStateEnum.UNKNOWN; + + + + + + + /** + * Creates a server connection, using a given socket. + * @param socket The socket which connects to the client. + * @param latestMessages Latest race snapshot to send to client. + * @throws IOException Thrown if there is a problem with the client socket. + */ + public ServerConnection(Socket socket, LatestMessages latestMessages) throws IOException { + this.socket = socket; + this.latestMessages = latestMessages; + + this.outputQueue = new LinkedBlockingQueue<>(); + this.inputQueue = new LinkedBlockingQueue<>(); + + + this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue); + this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue); + + new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser).start(); + new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser).start(); + + } + + + + @Override + public void run() { + try { + handshake(); + + } catch (HandshakeException e) { + Logger.getGlobal().log(Level.WARNING, "Server handshake failed.", e); + Thread.currentThread().interrupt(); + return; + } + + } + + + /** + * Initiates the handshake with the server. + * @throws HandshakeException Thrown if something goes wrong with the handshake. + */ + private void handshake() throws HandshakeException { + + //This function is a bit messy, and could probably be refactored a bit. + + connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE; + + + sendJoinAcceptanceMessage(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.controllerClient = new ControllerClient(inputQueue); + //new Thread(controllerClient, "ServerConnection.run()->ControllerClient thread " + controllerClient).start(); + + } + + this.visualiserInput = new VisualiserInput(latestMessages, outputQueue); + new Thread(visualiserInput, "ServerConnection.run()->VisualiserInput thread " + visualiserInput).start(); + + + connectionState = ConnectionStateEnum.CONNECTED; + + } + + + /** + * Waits until the server sends a {@link JoinAcceptance} message, and returns it. + * @return The {@link JoinAcceptance} message. + * @throws HandshakeException Thrown if we get interrupted while waiting. + */ + private JoinAcceptance waitForJoinAcceptance() throws HandshakeException { + + try { + + + while (connectionState == ConnectionStateEnum.WAITING_FOR_HANDSHAKE) { + + AC35Data message = inputQueue.take(); + + //We need to wait until they actually send a join request. + if (message.getType() == MessageType.JOIN_ACCEPTANCE) { + return (JoinAcceptance) message; + } + + } + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " was interrupted while waiting on the incoming message queue.", e); + + } + + + throw new HandshakeException("Handshake was cancelled. Connection state is now: " + connectionState); + + } + + + /** + * Sends the server a {@link RequestToJoin} message. + * @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 { + + //Send them the source ID. + RequestToJoin requestToJoin = new RequestToJoin(requestType); + + try { + outputQueue.put(requestToJoin); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " interrupted while placing RequestToJoin message on outgoing message queue.", e); + } + + } + + + /** + * Determines whether or not this connection is still alive. + * This is based off whether the {@link MessageSerialiser} is still alive. + * @return True if it is alive, false otherwise. + */ + public boolean isAlive() { + return messageSerialiser.isRunning(); + } + + +} diff --git a/racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java b/racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java new file mode 100644 index 00000000..7240e01b --- /dev/null +++ b/racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java @@ -0,0 +1,126 @@ +package mock.model; + +import mock.exceptions.SourceIDAllocationException; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + + +/** + * Tests if allocating source IDs works. + */ +public class SourceIdAllocatorTest { + + /** + * This is the list of source IDs that we start with. + */ + private List originalSourceIDs; + + /** + * Used to allocate source IDs. + */ + private SourceIdAllocator sourceIdAllocator; + + + @Before + public void setUp() throws Exception { + + originalSourceIDs = new ArrayList<>(); + originalSourceIDs.add(120); + originalSourceIDs.add(121); + originalSourceIDs.add(122); + originalSourceIDs.add(123); + originalSourceIDs.add(124); + originalSourceIDs.add(125); + + + sourceIdAllocator = new SourceIdAllocator(originalSourceIDs); + + } + + + /** + * Tests that allocation fails when we don't have any source IDs to allocate. + */ + @Test + public void emptyAllocationTest() { + + SourceIdAllocator allocator = new SourceIdAllocator(new ArrayList<>()); + + + try { + int sourceID = allocator.allocateSourceID(); + + fail("Exception should have been thrown, but wasn't."); + + } catch (SourceIDAllocationException e) { + + //We expect this exception to be thrown - success. + + } + + } + + + /** + * Tests that we can allocate a source ID. + * @throws Exception Thrown in case of error. + */ + @Test + public void allocationTest() throws Exception { + + + int sourceID = sourceIdAllocator.allocateSourceID(); + + } + + + /** + * Tests that we can allocate source IDs, but it will eventually be unable to allocate source IDs. + */ + @Test + public void allocationEventuallyFailsTest() { + + while (true) { + + try { + int sourceID = sourceIdAllocator.allocateSourceID(); + + } catch (SourceIDAllocationException e) { + //We expect to encounter this exception after enough allocations - success. + break; + + } + + } + + } + + + /** + * Tests if we can allocate a source ID, return it, and reallocate it. + * @throws Exception Thrown in case of error. + */ + @Test + public void reallocationTest() throws Exception { + + List sourceIDList = new ArrayList<>(); + sourceIDList.add(123); + + SourceIdAllocator sourceIdAllocator = new SourceIdAllocator(sourceIDList); + + //Allocate. + int sourceID = sourceIdAllocator.allocateSourceID(); + + //Return. + sourceIdAllocator.returnSourceID(sourceID); + + //Reallocate. + int sourceID2 = sourceIdAllocator.allocateSourceID(); + + } +} diff --git a/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java new file mode 100644 index 00000000..c3d0df04 --- /dev/null +++ b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java @@ -0,0 +1,31 @@ +package mock.model.commandFactory; + +import mock.model.MockRace; +import network.Messages.Enums.BoatActionEnum; +import org.junit.Before; +import org.junit.Test; +import shared.model.Boat; +import shared.model.Race; +import visualiser.model.VisualiserRace; + +import static org.testng.Assert.*; + +/** + * Created by connortaylorbrown on 4/08/17. + */ +public class WindCommandTest { + private Race race; + private Boat boat; + private Command upwind; + private Command downwind; + + @Before + public void setUp() { + boat = new Boat(0, "Bob", "NZ"); + } + + @Test + public void upwindCommandDecreasesAngle() { + + } +} \ No newline at end of file From 61d18f85c5354d6f3ac80708a4449b666def8b13 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Thu, 10 Aug 2017 23:57:54 +1200 Subject: [PATCH 02/16] javadoc fixes. --- racevisionGame/src/main/java/mock/model/MockRace.java | 1 + .../java/visualiser/gameController/ControllerClient.java | 2 +- racevisionGame/src/test/java/mock/model/MockRaceTest.java | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 94fc8792..2f23712f 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -56,6 +56,7 @@ public class MockRace extends Race { * @param latestMessages The LatestMessages to send events to. * @param polars The polars table to be used for boat simulation. * @param timeScale The timeScale for the race. See {@link Constants#RaceTimeScale}. + * @param windGenerator The wind generator used for the race. */ public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale, WindGenerator windGenerator) { diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java index 9b38a5ca..57459868 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java +++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java @@ -39,7 +39,7 @@ public class ControllerClient { /** * Send a keypress to server * @param key to send - * @throws IOException if socket write fails + * @throws InterruptedException If thread is interrupted while writing message to outgoing message queue. */ public void sendKey(ControlKey key) throws InterruptedException { BoatActionEnum protocolCode = key.getProtocolCode(); diff --git a/racevisionGame/src/test/java/mock/model/MockRaceTest.java b/racevisionGame/src/test/java/mock/model/MockRaceTest.java index 3c01b321..402e37f0 100644 --- a/racevisionGame/src/test/java/mock/model/MockRaceTest.java +++ b/racevisionGame/src/test/java/mock/model/MockRaceTest.java @@ -19,9 +19,9 @@ public class MockRaceTest { * Creates a MockRace for use in testing. * Has a constant wind generator. * @return MockRace for testing. - * @throws InvalidBoatDataException - * @throws InvalidRaceDataException - * @throws InvalidRegattaDataException + * @throws InvalidBoatDataException If the BoatDataSource cannot be created. + * @throws InvalidRaceDataException If the RaceDataSource cannot be created. + * @throws InvalidRegattaDataException If the RegattaDataSource cannot be created. */ public static MockRace createMockRace() throws InvalidBoatDataException, InvalidRaceDataException, InvalidRegattaDataException { From e4b72fdfeb9b3c086e1d16dfdcd23a3bd86a47cd Mon Sep 17 00:00:00 2001 From: zwu18 Date: Fri, 11 Aug 2017 20:38:54 +1200 Subject: [PATCH 03/16] Update appearance on client arrow. #story[1095] --- .../java/visualiser/model/ResizableRaceCanvas.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index 16393228..fd2ce085 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -358,15 +358,15 @@ public class ResizableRaceCanvas extends ResizableCanvas { //The x coordinates of each vertex of the boat. double[] x = { - pos.getX() - 12, + pos.getX() - 9, pos.getX(), - pos.getX() + 12 }; + pos.getX() + 9 }; //The y coordinates of each vertex of the boat. double[] y = { - pos.getY() + 24, - pos.getY() - 24, - pos.getY() + 24 }; + pos.getY() + 15, + pos.getY() - 15, + pos.getY() + 15 }; //The above shape is essentially a triangle 24px wide, and 48 long. From b625b6ab054590d98a18dd6003d1bd1659a1fe41 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Fri, 11 Aug 2017 22:38:39 +1200 Subject: [PATCH 04/16] XMLReader.readXMLFileToString no longer throws a Transformer exception. --- racevisionGame/src/main/java/mock/app/Event.java | 2 +- .../src/main/java/shared/dataInput/XMLReader.java | 9 ++++++--- .../src/test/java/network/BinaryMessageDecoderTest.java | 2 +- .../network/MessageDecoders/XMLMessageDecoderTest.java | 2 +- .../test/java/shared/dataInput/BoatXMLReaderTest.java | 2 +- .../test/java/shared/dataInput/RaceXMLReaderTest.java | 2 +- .../test/java/shared/dataInput/RegattaXMLReaderTest.java | 2 +- 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 5c6dd193..ecd72cb8 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -87,7 +87,7 @@ public class Event { this.boatXML = XMLReader.readXMLFileToString(boatsXMLFile, StandardCharsets.UTF_8); this.regattaXML = XMLReader.readXMLFileToString(regattaXMLFile, StandardCharsets.UTF_8); - } catch (TransformerException | XMLReaderException e) { + } catch (XMLReaderException e) { throw new EventConstructionException("Could not read XML files.", e); } diff --git a/racevisionGame/src/main/java/shared/dataInput/XMLReader.java b/racevisionGame/src/main/java/shared/dataInput/XMLReader.java index 04c6c1f7..dd5a4d6d 100644 --- a/racevisionGame/src/main/java/shared/dataInput/XMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/XMLReader.java @@ -166,10 +166,9 @@ public abstract class XMLReader { * @param path path of the XML * @param encoding encoding of the xml * @return A string containing the contents of the specified file. - * @throws TransformerException Issue with the XML format * @throws XMLReaderException Thrown if file cannot be read for some reason. */ - public static String readXMLFileToString(String path, Charset encoding) throws TransformerException, XMLReaderException { + public static String readXMLFileToString(String path, Charset encoding) throws XMLReaderException { InputStream fileStream = XMLReader.class.getClassLoader().getResourceAsStream(path); @@ -182,7 +181,11 @@ public abstract class XMLReader { doc.getDocumentElement().normalize(); - return XMLReader.getContents(doc); + try { + return XMLReader.getContents(doc); + } catch (TransformerException e) { + throw new XMLReaderException("Could not get XML file contents.", e); + } } diff --git a/racevisionGame/src/test/java/network/BinaryMessageDecoderTest.java b/racevisionGame/src/test/java/network/BinaryMessageDecoderTest.java index 55ac00be..46441775 100644 --- a/racevisionGame/src/test/java/network/BinaryMessageDecoderTest.java +++ b/racevisionGame/src/test/java/network/BinaryMessageDecoderTest.java @@ -91,7 +91,7 @@ public class BinaryMessageDecoderTest { XMLMessageDecoderTest.compareXMLMessages(xmlMessage, xmlMessageDecoded); - } catch (XMLReaderException | TransformerException e){ + } catch (XMLReaderException e){ fail("couldn't read file" + e.getMessage()); } } diff --git a/racevisionGame/src/test/java/network/MessageDecoders/XMLMessageDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/XMLMessageDecoderTest.java index f0c0ba0b..a71f3f34 100644 --- a/racevisionGame/src/test/java/network/MessageDecoders/XMLMessageDecoderTest.java +++ b/racevisionGame/src/test/java/network/MessageDecoders/XMLMessageDecoderTest.java @@ -53,7 +53,7 @@ public class XMLMessageDecoderTest { compareXMLMessages(message, decodedMessage); - } catch (XMLReaderException | TransformerException e){ + } catch (XMLReaderException e){ fail("couldn't read file" + e.getMessage()); } diff --git a/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java b/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java index 60ec2be9..029447d6 100644 --- a/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java +++ b/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java @@ -38,7 +38,7 @@ public class BoatXMLReaderTest { try { boatXMLString = XMLReader.readXMLFileToString("mock/mockXML/boatTest.xml", StandardCharsets.UTF_8); - } catch (TransformerException | XMLReaderException e) { + } catch (XMLReaderException e) { throw new InvalidBoatDataException("Could not read boat XML file into a string.", e); } diff --git a/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java b/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java index 6080919d..1f4e72f6 100644 --- a/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java +++ b/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java @@ -18,7 +18,7 @@ public class RaceXMLReaderTest { try { raceXMLString = XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8); - } catch (TransformerException | XMLReaderException e) { + } catch (XMLReaderException e) { throw new InvalidRaceDataException("Could not read race XML file into a string.", e); } diff --git a/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java b/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java index 2f043c0d..d406af32 100644 --- a/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java +++ b/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java @@ -27,7 +27,7 @@ public class RegattaXMLReaderTest { try { regattaXMLString = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8); - } catch (TransformerException | XMLReaderException e) { + } catch (XMLReaderException e) { throw new InvalidRegattaDataException("Could not read regatta XML file into a string.", e); } From 6e5fb62880a4fb3aebcbd7bcfd717d88a59dac83 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Sat, 12 Aug 2017 01:05:45 +1200 Subject: [PATCH 05/16] Added REQUEST_RECEIVED and DECLINED to ConnnectionStateEnum. #story[1095] --- racevisionGame/src/main/java/mock/app/Event.java | 14 ++++---------- .../main/java/mock/enums/ConnectionStateEnum.java | 15 +++++++++++++-- .../shared/model/RunnableWithFramePeriod.java | 11 ----------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index ecd72cb8..b5b9865f 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -19,8 +19,10 @@ import javax.xml.transform.TransformerException; import java.io.IOException; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -70,7 +72,7 @@ public class Event { */ public Event(boolean singlePlayer) throws EventConstructionException { - singlePlayer = false; + singlePlayer = false;//TEMP String raceXMLFile = "mock/mockXML/raceTest.xml"; String boatsXMLFile = "mock/mockXML/boatTest.xml"; @@ -154,14 +156,6 @@ public class Event { - public String getAddress() throws UnknownHostException { - return connectionAcceptor.getAddress(); - } - - public int getPort() { - return connectionAcceptor.getServerPort(); - } - /** * Sends the initial race data and then begins race simulation. */ @@ -190,7 +184,7 @@ public class Event { private String getRaceXMLAtCurrentTime(String raceXML) { //The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute. - long millisecondsToAdd = Constants.RacePreStartTime + (1 * 60 * 1000); + long millisecondsToAdd = Constants.RacePreStartTime + Duration.ofMinutes(1).toMillis(); long secondsToAdd = millisecondsToAdd / 1000; //Scale the time using our time scalar. secondsToAdd = secondsToAdd / Constants.RaceTimeScale; diff --git a/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java b/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java index 4d4961bb..79817ef9 100644 --- a/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java +++ b/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java @@ -15,15 +15,26 @@ public enum ConnectionStateEnum { */ WAITING_FOR_HANDSHAKE(1), + /** + * The server has receved a {@link network.Messages.RequestToJoin} from the client. + */ + REQUEST_RECEIVED(2), + /** * The client has completed the handshake, and is connected. + * That is, the client sent a {@link network.Messages.RequestToJoin}, which was successful, and the server responded with a {@link network.Messages.JoinAcceptance}. */ - CONNECTED(2), + CONNECTED(3), /** * The client has timed out. */ - TIMED_OUT(3); + TIMED_OUT(4), + + /** + * The client's connection has been declined. + */ + DECLINED(5); diff --git a/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java index af633af3..7ab12532 100644 --- a/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java +++ b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java @@ -1,17 +1,6 @@ package shared.model; -import network.Exceptions.InvalidMessageException; -import network.MessageEncoders.RaceVisionByteEncoder; -import network.Messages.AC35Data; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; From 89b0aa8b77209f0e7f576b101a4d35bb43bda739 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Sat, 12 Aug 2017 19:58:47 +1200 Subject: [PATCH 06/16] Implemented MessageRouter. Added ConnectionToServerCommandFactory, and JoinSuccessfulCommand, RaceParticipantsFullCOmmand, ServerFullCommand. Added IncomingHeartBeatCommandFactory, and IncomingHeartBeatCommand. Added ConnectionToServerState, which represents the client's connection state to server. Renamed VisualiserInput to VisualiserRaceController. Added ConnectionToServer, which tracks the client's connection to server. Added ConnectionToServerController, which accepts JoinAcceptance messages, turns them into commands, and passes them to ConnectionToServer. Added IncomingHeartBeatService, which tracks the heart beat status of the connection. Added IncomingHeartBeatController, which accepts HeartBeat messages, turns them into commands, and passes them to IncomingHeartBeatService. Refactored ServerConnection a bit. #story[1095] --- .../java/mock/model/HeartBeatService.java | 2 +- .../network/MessageRouters/MessageRouter.java | 118 ++++- .../StreamRelated/MessageDeserialiser.java | 8 +- .../StreamRelated/MessageSerialiser.java | 8 + .../ConnectionToServerCommandFactory.java | 41 ++ .../JoinSuccessfulCommand.java | 47 ++ .../RaceParticipantsFullCommand.java | 47 ++ .../ServerFullCommand.java | 47 ++ .../IncomingHeartBeatCommand.java | 47 ++ .../IncomingHeartBeatCommandFactory.java | 33 ++ .../Controllers/MainController.java | 3 +- .../Controllers/RaceController.java | 3 +- .../Controllers/StartController.java | 6 +- .../enums/ConnectionToServerState.java | 96 ++++ .../visualiser/model/ServerConnection.java | 243 ---------- .../VisualiserRaceController.java} | 40 +- .../network/ConnectionToServer.java | 157 ++++++ .../network/ConnectionToServerController.java | 78 +++ .../network/IncomingHeartBeatController.java | 78 +++ .../network/IncomingHeartBeatService.java | 109 +++++ .../visualiser/network/ServerConnection.java | 459 ++++++++++++++++++ .../ConnectionToServerParticipantTest.java | 150 ++++++ .../ConnectionToServerSpectatorTest.java | 109 +++++ 23 files changed, 1640 insertions(+), 289 deletions(-) create mode 100644 racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java create mode 100644 racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java delete mode 100644 racevisionGame/src/main/java/visualiser/model/ServerConnection.java rename racevisionGame/src/main/java/visualiser/{app/VisualiserInput.java => model/VisualiserRaceController.java} (82%) create mode 100644 racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java create mode 100644 racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java create mode 100644 racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java create mode 100644 racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java create mode 100644 racevisionGame/src/main/java/visualiser/network/ServerConnection.java create mode 100644 racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java create mode 100644 racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java diff --git a/racevisionGame/src/main/java/mock/model/HeartBeatService.java b/racevisionGame/src/main/java/mock/model/HeartBeatService.java index 232eb9ad..0028a575 100644 --- a/racevisionGame/src/main/java/mock/model/HeartBeatService.java +++ b/racevisionGame/src/main/java/mock/model/HeartBeatService.java @@ -22,7 +22,7 @@ public class HeartBeatService implements RunnableWithFramePeriod { /** * Period for the heartbeat - that is, how often we send it. Milliseconds. */ - private long heartbeatPeriod = 5000; + private long heartbeatPeriod = 2500; /** diff --git a/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java index 4eaa6dce..b767b880 100644 --- a/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java +++ b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java @@ -1,11 +1,127 @@ package network.MessageRouters; +import network.Messages.AC35Data; +import network.Messages.Enums.MessageType; +import org.jetbrains.annotations.NotNull; +import shared.model.RunnableWithFramePeriod; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + /** * This class routes {@link network.Messages.AC35Data} messages to an appropriate message controller. */ -public class MessageRouter { +public class MessageRouter implements RunnableWithFramePeriod { + + + /** + * Incoming queue of messages. + */ + private BlockingQueue incomingMessages; + + + /** + * The routing map, which maps from a {@link MessageType} to a message queue. + */ + private Map> routeMap = new HashMap<>(); + + + /** + * The default routing queue. + * Messages without routes are sent here. + * Nothing by default, which means unrouted messages are discarded + */ + private Optional> defaultRoute = Optional.empty(); + + + + /** + * Constructs a {@link MessageRouter} with a given incoming message queue. + * @param incomingMessages Incoming message queue to read from. + */ + public MessageRouter(BlockingQueue incomingMessages) { + this.incomingMessages = incomingMessages; + } + + + /** + * Returns the queue the message router reads from. + * Place messages onto this queue to pass them to the router. + * @return Queue the message router reads from. + */ + public BlockingQueue getIncomingMessageQueue() { + return incomingMessages; + } + + + /** + * Adds a route, which routes a given type of message to a given queue. + * @param messageType The message type to route. + * @param queue The queue to route messages to. + */ + public void addRoute(MessageType messageType, BlockingQueue queue) { + routeMap.put(messageType, queue); + } + + /** + * Removes the route for a given {@link MessageType}. + * @param messageType MessageType to remove route for. + */ + public void removeRoute(MessageType messageType) { + routeMap.remove(messageType); + } + + /** + * Adds a given queue as the default route for any unrouted message types. + * @param queue Queue to use as default route. + */ + public void addDefaultRoute(@NotNull BlockingQueue queue) { + defaultRoute = Optional.of(queue); + } + + /** + * Removes the current default route, if it exists. + */ + public void removeDefaultRoute() { + defaultRoute = Optional.empty(); + } + + + + @Override + public void run() { + + while (!Thread.interrupted()) { + + try { + + AC35Data message = incomingMessages.take(); + + + if (routeMap.containsKey(message.getType())) { + //We have a route. + routeMap.get(message.getType()).put(message); + + } else { + //No route. Use default. + if (defaultRoute.isPresent()) { + defaultRoute.get().put(message); + } + } + + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "MessageRouter: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading message.", e); + Thread.currentThread().interrupt(); + } + } + } } diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java index 028e01a9..2dbbe4e0 100644 --- a/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java @@ -50,7 +50,13 @@ public class MessageDeserialiser implements RunnableWithFramePeriod { this.messagesRead = messagesRead; } - + /** + * Returns the queue of messages read from the socket. + * @return Queue of messages read from socket. + */ + public BlockingQueue getMessagesRead() { + return messagesRead; + } /** diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java index 8d2a7038..84bddf78 100644 --- a/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java @@ -54,6 +54,14 @@ public class MessageSerialiser implements RunnableWithFramePeriod { this.messagesToSend = messagesToSend; } + /** + * Returns the queue of messages to write to the socket. + * @return Queue of messages to write to the socket. + */ + public BlockingQueue getMessagesToSend() { + return messagesToSend; + } + /** * Increments the ackNumber value, and returns it. diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java new file mode 100644 index 00000000..bac06db3 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java @@ -0,0 +1,41 @@ +package visualiser.Commands.ConnectionToServerCommands; + + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.JoinAcceptance; +import visualiser.network.ConnectionToServer; + +/** + * Factory to create ConnectionToServer commands. + */ +public class ConnectionToServerCommandFactory { + + /** + * Generates a command to execute on server connection based on the type of {@link network.Messages.Enums.JoinAcceptanceEnum}. + * @param message The message to turn into a command. + * @param connectionToServer The connection for the command to operate on. + * @return The command to execute the given action. + * @throws CommandConstructionException Thrown if the command cannot be constructed. + */ + public static Command create(AC35Data message, ConnectionToServer connectionToServer) throws CommandConstructionException { + + if (!(message instanceof JoinAcceptance)) { + throw new CommandConstructionException("Message: " + message + " is not a JoinAcceptance message."); + } + + JoinAcceptance joinAcceptance = (JoinAcceptance) message; + + + switch(joinAcceptance.getAcceptanceType()) { + + case JOIN_SUCCESSFUL: return new JoinSuccessfulCommand(joinAcceptance, connectionToServer); + case RACE_PARTICIPANTS_FULL: return new RaceParticipantsFullCommand(joinAcceptance, connectionToServer); + case SERVER_FULL: return new ServerFullCommand(joinAcceptance, connectionToServer); + + default: throw new CommandConstructionException("Could not create command for JoinAcceptance: " + joinAcceptance + ". Unknown JoinAcceptanceEnum."); + } + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java new file mode 100644 index 00000000..8cdfea5b --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java @@ -0,0 +1,47 @@ +package visualiser.Commands.ConnectionToServerCommands; + +import mock.model.commandFactory.Command; +import network.Messages.JoinAcceptance; +import visualiser.enums.ConnectionToServerState; +import visualiser.network.ConnectionToServer; + +import java.util.Optional; + + +/** + * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL} {@link network.Messages.JoinAcceptance} message is received. + */ +public class JoinSuccessfulCommand implements Command { + + /** + * The message to operate on. + */ + private JoinAcceptance joinAcceptance; + + /** + * The context to operate on. + */ + private ConnectionToServer connectionToServer; + + + /** + * Creates a new {@link JoinSuccessfulCommand}, which operates on a given {@link ConnectionToServer}. + * @param joinAcceptance The message to operate on. + * @param connectionToServer The context to operate on. + */ + public JoinSuccessfulCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { + this.joinAcceptance = joinAcceptance; + this.connectionToServer = connectionToServer; + } + + + + @Override + public void execute() { + + connectionToServer.setJoinAcceptance(joinAcceptance); + + connectionToServer.setConnectionState(ConnectionToServerState.CONNECTED); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java new file mode 100644 index 00000000..ae631c6e --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java @@ -0,0 +1,47 @@ +package visualiser.Commands.ConnectionToServerCommands; + +import mock.model.commandFactory.Command; +import network.Messages.JoinAcceptance; +import visualiser.enums.ConnectionToServerState; +import visualiser.network.ConnectionToServer; + +import java.util.Optional; + + +/** + * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#RACE_PARTICIPANTS_FULL} {@link JoinAcceptance} message is received. + */ +public class RaceParticipantsFullCommand implements Command { + + /** + * The message to operate on. + */ + private JoinAcceptance joinAcceptance; + + /** + * The context to operate on. + */ + private ConnectionToServer connectionToServer; + + + /** + * Creates a new {@link RaceParticipantsFullCommand}, which operates on a given {@link ConnectionToServer}. + * @param joinAcceptance The message to operate on. + * @param connectionToServer The context to operate on. + */ + public RaceParticipantsFullCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { + this.joinAcceptance = joinAcceptance; + this.connectionToServer = connectionToServer; + } + + + + @Override + public void execute() { + + connectionToServer.setJoinAcceptance(joinAcceptance); + + connectionToServer.setConnectionState(ConnectionToServerState.DECLINED); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java new file mode 100644 index 00000000..e88d2c16 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java @@ -0,0 +1,47 @@ +package visualiser.Commands.ConnectionToServerCommands; + +import mock.model.commandFactory.Command; +import network.Messages.JoinAcceptance; +import visualiser.enums.ConnectionToServerState; +import visualiser.network.ConnectionToServer; + +import java.util.Optional; + + +/** + * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#SERVER_FULL} {@link JoinAcceptance} message is received. + */ +public class ServerFullCommand implements Command { + + /** + * The message to operate on. + */ + private JoinAcceptance joinAcceptance; + + /** + * The context to operate on. + */ + private ConnectionToServer connectionToServer; + + + /** + * Creates a new {@link ServerFullCommand}, which operates on a given {@link ConnectionToServer}. + * @param joinAcceptance The message to operate on. + * @param connectionToServer The context to operate on. + */ + public ServerFullCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { + this.joinAcceptance = joinAcceptance; + this.connectionToServer = connectionToServer; + } + + + + @Override + public void execute() { + + connectionToServer.setJoinAcceptance(joinAcceptance); + + connectionToServer.setConnectionState(ConnectionToServerState.DECLINED); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java new file mode 100644 index 00000000..d4668ef7 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java @@ -0,0 +1,47 @@ +package visualiser.Commands.IncomingHeartBeatCommands; + +import mock.model.commandFactory.Command; +import network.Messages.HeartBeat; +import network.Messages.JoinAcceptance; +import visualiser.enums.ConnectionToServerState; +import visualiser.network.ConnectionToServer; +import visualiser.network.IncomingHeartBeatService; + + +/** + * Command created when a {@link HeartBeat} message is received. + */ +public class IncomingHeartBeatCommand implements Command { + + /** + * The message to operate on. + */ + private HeartBeat heartBeat; + + /** + * The context to operate on. + */ + private IncomingHeartBeatService incomingHeartBeatService; + + + /** + * Creates a new {@link IncomingHeartBeatCommand}, which operates on a given {@link IncomingHeartBeatService}. + * @param heartBeat The message to operate on. + * @param incomingHeartBeatService The context to operate on. + */ + public IncomingHeartBeatCommand(HeartBeat heartBeat, IncomingHeartBeatService incomingHeartBeatService) { + this.heartBeat = heartBeat; + this.incomingHeartBeatService = incomingHeartBeatService; + } + + + + @Override + public void execute() { + + incomingHeartBeatService.setLastHeartBeatSeqNum(heartBeat.getSequenceNumber()); + + incomingHeartBeatService.setLastHeartbeatTime(System.currentTimeMillis()); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java new file mode 100644 index 00000000..bab68c42 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java @@ -0,0 +1,33 @@ +package visualiser.Commands.IncomingHeartBeatCommands; + + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.HeartBeat; +import visualiser.network.IncomingHeartBeatService; + +/** + * Factory to create IncomingHeartBeatService commands. + */ +public class IncomingHeartBeatCommandFactory { + + /** + * Generates a command on an IncomingHeartBeatService. + * @param message The message to turn into a command. + * @param incomingHeartBeatService The context for the command to operate on. + * @return The command to execute the given action. + * @throws CommandConstructionException Thrown if the command cannot be constructed. + */ + public static Command create(AC35Data message, IncomingHeartBeatService incomingHeartBeatService) throws CommandConstructionException { + + if (!(message instanceof HeartBeat)) { + throw new CommandConstructionException("Message: " + message + " is not a HeartBeat message."); + } + + HeartBeat heartBeat = (HeartBeat) message; + + return new IncomingHeartBeatCommand(heartBeat, incomingHeartBeatService); + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java index 900e43b0..c801f16a 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java @@ -3,9 +3,8 @@ package visualiser.Controllers; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.layout.AnchorPane; -import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; -import visualiser.model.ServerConnection; +import visualiser.network.ServerConnection; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRace; diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index 0a5fb33a..2e1a811e 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -17,13 +17,12 @@ import javafx.scene.layout.StackPane; import javafx.util.Callback; import network.Messages.Enums.RaceStatusEnum; import shared.model.Leg; -import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; import visualiser.gameController.Keys.ControlKey; import visualiser.gameController.Keys.KeyFactory; import visualiser.model.*; +import visualiser.network.ServerConnection; -import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; import java.util.logging.Level; diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index 82ac39f6..b1c17bb1 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -11,6 +11,7 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.paint.Color; import network.Messages.Enums.RaceStatusEnum; +import network.Messages.Enums.RequestToJoinEnum; import network.Messages.LatestMessages; import shared.dataInput.*; import shared.enums.XMLFileType; @@ -18,9 +19,8 @@ import shared.exceptions.InvalidBoatDataException; import shared.exceptions.InvalidRaceDataException; import shared.exceptions.InvalidRegattaDataException; import shared.exceptions.XMLReaderException; -import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; -import visualiser.model.ServerConnection; +import visualiser.network.ServerConnection; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRace; @@ -327,7 +327,7 @@ public class StartController extends Controller implements Observer { try { LatestMessages latestMessages = new LatestMessages(); - this.serverConnection = new ServerConnection(socket, latestMessages); + this.serverConnection = new ServerConnection(socket, latestMessages, RequestToJoinEnum.PARTICIPANT); this.controllerClient = serverConnection.getControllerClient(); diff --git a/racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java b/racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java new file mode 100644 index 00000000..22e1e30f --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java @@ -0,0 +1,96 @@ +package visualiser.enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * The states in which a connection from a client to a server may have. + */ +public enum ConnectionToServerState { + + UNKNOWN(0), + + /** + * We're waiting for the server to complete the joining handshake. + * See {@link network.Messages.RequestToJoin} and {@link network.Messages.JoinAcceptance}. + */ + REQUEST_SENT(1), + + /** + * The client has receved a {@link network.Messages.JoinAcceptance} from the server. + */ + RESPONSE_RECEIVED(2), + + /** + * The server has completed the handshake, and is connected. + * That is, the client sent a {@link network.Messages.RequestToJoin}, which was successful, and the server responded with a {@link network.Messages.JoinAcceptance}. + */ + CONNECTED(3), + + /** + * The server has timed out, or the connection has been interrupted. + */ + TIMED_OUT(4), + + /** + * The client's connection has been declined. + */ + DECLINED(5); + + + + + private byte value; + + /** + * Ctor. Creates a ConnectionToServerState from a given primitive integer value, cast to a byte. + * @param value Integer, which is cast to byte, to construct from. + */ + private ConnectionToServerState(int value) { + this.value = (byte) value; + } + + /** + * Returns the primitive value of the enum. + * @return Primitive value of the enum. + */ + public byte getValue() { + return value; + } + + + /** + * Stores a mapping between Byte values and ConnectionToServerState values. + */ + private static final Map byteToStateMap = new HashMap<>(); + + + /* + Static initialization block. Initializes the byteToStateMap. + */ + static { + for (ConnectionToServerState type : ConnectionToServerState.values()) { + ConnectionToServerState.byteToStateMap.put(type.value, type); + } + } + + + /** + * Returns the enumeration value which corresponds to a given byte value. + * @param connectionState Byte value to convert to a ConnectionToServerState value. + * @return The ConnectionToServerState value which corresponds to the given byte value. + */ + public static ConnectionToServerState fromByte(byte connectionState) { + //Gets the corresponding ConnectionToServerState from the map. + ConnectionToServerState type = ConnectionToServerState.byteToStateMap.get(connectionState); + + if (type == null) { + //If the byte value wasn't found, return the UNKNOWN ConnectionToServerState. + return ConnectionToServerState.UNKNOWN; + } else { + //Otherwise, return the ConnectionToServerState. + return type; + } + + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/ServerConnection.java b/racevisionGame/src/main/java/visualiser/model/ServerConnection.java deleted file mode 100644 index 742ab968..00000000 --- a/racevisionGame/src/main/java/visualiser/model/ServerConnection.java +++ /dev/null @@ -1,243 +0,0 @@ -package visualiser.model; - - -import mock.enums.ConnectionStateEnum; -import network.Messages.AC35Data; -import network.Messages.Enums.JoinAcceptanceEnum; -import network.Messages.Enums.MessageType; -import network.Messages.Enums.RequestToJoinEnum; -import network.Messages.JoinAcceptance; -import network.Messages.LatestMessages; -import network.Messages.RequestToJoin; -import network.StreamRelated.MessageDeserialiser; -import network.StreamRelated.MessageSerialiser; -import shared.exceptions.HandshakeException; -import visualiser.app.VisualiserInput; -import visualiser.gameController.ControllerClient; - -import java.io.IOException; -import java.net.Socket; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * This class handles the client-server connection handshake, and creation of VisualiserInput and ControllerClient. - */ -public class ServerConnection implements Runnable { - - /** - * The socket for the connection to server. - */ - 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. - */ - private LatestMessages latestMessages; - - - /** - * Used to convert incoming messages into a race snapshot. - */ - private VisualiserInput visualiserInput; - - /** - * Used to send client input to server. - */ - private ControllerClient controllerClient; - - - /** - * Used to write messages to socket. - */ - private MessageSerialiser messageSerialiser; - - /** - * Stores messages to write to socket. - */ - private BlockingQueue outputQueue; - - /** - * Used to read messages from socket. - */ - private MessageDeserialiser messageDeserialiser; - - /** - * Stores messages read from socket. - */ - private BlockingQueue inputQueue; - - /** - * The state of the connection to the client. - */ - private ConnectionStateEnum connectionState = ConnectionStateEnum.UNKNOWN; - - - - - - - /** - * Creates a server connection, using a given socket. - * @param socket The socket which connects to the client. - * @param latestMessages Latest race snapshot to send to client. - * @throws IOException Thrown if there is a problem with the client socket. - */ - public ServerConnection(Socket socket, LatestMessages latestMessages) throws IOException { - this.socket = socket; - this.latestMessages = latestMessages; - - this.outputQueue = new LinkedBlockingQueue<>(); - this.inputQueue = new LinkedBlockingQueue<>(); - - - this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue); - this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue); - - new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser).start(); - new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser).start(); - - - this.controllerClient = new ControllerClient(outputQueue); - - } - - - - @Override - public void run() { - try { - handshake(); - - } catch (HandshakeException e) { - Logger.getGlobal().log(Level.WARNING, "Server handshake failed.", e); - Thread.currentThread().interrupt(); - return; - } - - } - - - /** - * Initiates the handshake with the server. - * @throws HandshakeException Thrown if something goes wrong with the handshake. - */ - private void handshake() throws HandshakeException { - - //This function is a bit messy, and could probably be refactored a bit. - - connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE; - - - sendRequestToJoinMessage(RequestToJoinEnum.PARTICIPANT); - - - JoinAcceptance joinAcceptance = waitForJoinAcceptance(); - - - //If we join successfully... - if (joinAcceptance.getAcceptanceType() == JoinAcceptanceEnum.JOIN_SUCCESSFUL) { - - this.allocatedSourceID = joinAcceptance.getSourceID(); - - - //new Thread(controllerClient, "ServerConnection.run()->ControllerClient thread " + controllerClient).start(); - - } - - this.visualiserInput = new VisualiserInput(latestMessages, inputQueue); - new Thread(visualiserInput, "ServerConnection.run()->VisualiserInput thread " + visualiserInput).start(); - - - connectionState = ConnectionStateEnum.CONNECTED; - - } - - - /** - * Waits until the server sends a {@link JoinAcceptance} message, and returns it. - * @return The {@link JoinAcceptance} message. - * @throws HandshakeException Thrown if we get interrupted while waiting. - */ - private JoinAcceptance waitForJoinAcceptance() throws HandshakeException { - - try { - - - while (connectionState == ConnectionStateEnum.WAITING_FOR_HANDSHAKE) { - - AC35Data message = inputQueue.take(); - - //We need to wait until they actually send a join request. - if (message.getType() == MessageType.JOIN_ACCEPTANCE) { - return (JoinAcceptance) message; - } - - } - - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " was interrupted while waiting on the incoming message queue.", e); - - } - - - throw new HandshakeException("Handshake was cancelled. Connection state is now: " + connectionState); - - } - - - /** - * Sends the server a {@link RequestToJoin} message. - * @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 sendRequestToJoinMessage(RequestToJoinEnum requestType) throws HandshakeException { - - //Send them the source ID. - RequestToJoin requestToJoin = new RequestToJoin(requestType); - - try { - outputQueue.put(requestToJoin); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " interrupted while placing RequestToJoin message on outgoing message queue.", e); - } - - } - - - /** - * Determines whether or not this connection 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 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; - } -} diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java similarity index 82% rename from racevisionGame/src/main/java/visualiser/app/VisualiserInput.java rename to racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java index 0579b623..15adc13b 100644 --- a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java @@ -1,4 +1,4 @@ -package visualiser.app; +package visualiser.model; import network.Messages.*; import shared.model.RunnableWithFramePeriod; @@ -11,16 +11,7 @@ import java.util.logging.Logger; * TCP client which receives packets/messages from a race data source * (e.g., mock source, official source), and exposes them to any observers. */ -public class VisualiserInput implements RunnableWithFramePeriod { - - /** - * Timestamp of the last heartbeat. - */ - private long lastHeartbeatTime = -1; - /** - * Sequence number of the last heartbeat. - */ - private long lastHeartbeatSequenceNum = -1; +public class VisualiserRaceController implements RunnableWithFramePeriod { /** @@ -42,11 +33,9 @@ public class VisualiserInput implements RunnableWithFramePeriod { * @param latestMessages Object to place messages in. * @param incomingMessages The incoming queue of messages. */ - public VisualiserInput(LatestMessages latestMessages, BlockingQueue incomingMessages) { + public VisualiserRaceController(LatestMessages latestMessages, BlockingQueue incomingMessages) { this.latestMessages = latestMessages; this.incomingMessages = incomingMessages; - - this.lastHeartbeatTime = System.currentTimeMillis(); } @@ -59,15 +48,7 @@ public class VisualiserInput implements RunnableWithFramePeriod { return latestMessages; } - /** - * Calculates the time since last heartbeat, in milliseconds. - * - * @return Time since last heartbeat, in milliseconds.. - */ - private double timeSinceHeartbeat() { - long now = System.currentTimeMillis(); - return (now - lastHeartbeatTime); - } + @Override @@ -90,19 +71,6 @@ public class VisualiserInput implements RunnableWithFramePeriod { //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: { diff --git a/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java b/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java new file mode 100644 index 00000000..4d22fc23 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java @@ -0,0 +1,157 @@ +package visualiser.network; + + +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import network.Messages.RequestToJoin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import shared.model.RunnableWithFramePeriod; +import visualiser.enums.ConnectionToServerState; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class tracks the state of the connection to a server. + */ +public class ConnectionToServer implements RunnableWithFramePeriod { + + + /** + * The state of the connection to the client. + */ + private ConnectionToServerState connectionState = ConnectionToServerState.UNKNOWN; + + + /** + * The type of join request to make to server. + */ + private RequestToJoinEnum requestType; + + + /** + * The queue to place outgoing messages on. + */ + private BlockingQueue outgoingMessages; + + + + /** + * The {@link JoinAcceptance} message that has been received, if any. + */ + @Nullable + private JoinAcceptance joinAcceptance; + + + /** + * The incoming commands to execute. + */ + private BlockingQueue incomingCommands; + + + + /** + * Constructs a ConnectionToServer with a given state. + * @param connectionState The state of the connection. + * @param requestType The type of join request to make to server. + * @param incomingCommands The queue of commands to execute. + * @param outgoingMessages The queue to place outgoing messages on. + */ + public ConnectionToServer(ConnectionToServerState connectionState, RequestToJoinEnum requestType, BlockingQueue incomingCommands, BlockingQueue outgoingMessages) { + this.connectionState = connectionState; + this.requestType = requestType; + this.incomingCommands = incomingCommands; + this.outgoingMessages = outgoingMessages; + } + + + /** + * Returns the state of this connection. + * @return The state of this connection. + */ + public ConnectionToServerState getConnectionState() { + return connectionState; + } + + /** + * Sets the state of this connection. + * @param connectionState The new state of this connection. + */ + public void setConnectionState(ConnectionToServerState connectionState) { + this.connectionState = connectionState; + } + + + /** + * Returns the {@link JoinAcceptance} message received from the server, if any. + * @return The JoinAcceptance message from server. Null if no response from server. + */ + @Nullable + public JoinAcceptance getJoinAcceptance() { + return joinAcceptance; + } + + /** + * Sets the {@link JoinAcceptance} message received from the server, if any. + * @param joinAcceptance The new JoinAcceptance message from server. + */ + public void setJoinAcceptance(@NotNull JoinAcceptance joinAcceptance) { + this.joinAcceptance = joinAcceptance; + } + + + + @Override + public void run() { + + try { + sendRequestToJoinMessage(requestType); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "ConnectionToServer: " + this + " was interrupted on thread: " + Thread.currentThread() + " while sending RequestToJoin.", e); + Thread.currentThread().interrupt(); + + } + + while (!Thread.interrupted()) { + + try { + Command command = incomingCommands.take(); + command.execute(); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "ConnectionToServer: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading command.", e); + Thread.currentThread().interrupt(); + } + + } + + //If we get interrupted, we consider the connection to have timed-out. + connectionState = ConnectionToServerState.TIMED_OUT; + + } + + + + /** + * Sends the server a {@link RequestToJoin} message. + * @param requestType The type of request to send + * @throws InterruptedException Thrown if the thread is interrupted while placing message on the outgoing message queue. + */ + private void sendRequestToJoinMessage(RequestToJoinEnum requestType) throws InterruptedException { + + //Send them the source ID. + RequestToJoin requestToJoin = new RequestToJoin(requestType); + + outgoingMessages.put(requestToJoin); + + connectionState = ConnectionToServerState.REQUEST_SENT; + } + + + +} diff --git a/racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java b/racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java new file mode 100644 index 00000000..467bbd54 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java @@ -0,0 +1,78 @@ +package visualiser.network; + + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import shared.model.RunnableWithFramePeriod; +import visualiser.Commands.ConnectionToServerCommands.ConnectionToServerCommandFactory; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * The controller for connection related messages, coming from the server to the client. + */ +public class ConnectionToServerController implements RunnableWithFramePeriod { + + + /** + * The incoming queue of messages to act on. + */ + private BlockingQueue incomingMessages; + + + /** + * The connection we are acting on. + */ + private ConnectionToServer connectionToServer; + + + /** + * The queue to place commands on. + */ + private BlockingQueue outgoingCommands; + + + + /** + * Constructs a {@link ConnectionToServer} controller with the given parameters. + * This accepts connection related messages, converts them to commands, and passes them to an outgoing command queue. + * @param incomingMessages The message queue to read from. + * @param connectionToServer The ConnectionToServer (context) to act on. + * @param outgoingCommands The queue to place outgoing commands on. + */ + public ConnectionToServerController(BlockingQueue incomingMessages, ConnectionToServer connectionToServer, BlockingQueue outgoingCommands) { + this.incomingMessages = incomingMessages; + this.connectionToServer = connectionToServer; + this.outgoingCommands = outgoingCommands; + } + + + @Override + public void run() { + + while (!Thread.interrupted()) { + + try { + AC35Data message = incomingMessages.take(); + Command command = ConnectionToServerCommandFactory.create(message, connectionToServer); + outgoingCommands.put(command); + + } catch (CommandConstructionException e) { + Logger.getGlobal().log(Level.WARNING, "ConnectionToServerController: " + this + " could not create command from message.", e); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "ConnectionToServerController: " + this + " was interrupted on thread: " + Thread.currentThread(), e); + Thread.currentThread().interrupt(); + + } + + + } + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java new file mode 100644 index 00000000..739a3e64 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java @@ -0,0 +1,78 @@ +package visualiser.network; + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import shared.model.RunnableWithFramePeriod; +import visualiser.Commands.IncomingHeartBeatCommands.IncomingHeartBeatCommandFactory; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * The controller for heartbeat related messages, coming from the server to the client. + */ +public class IncomingHeartBeatController implements RunnableWithFramePeriod { + + + /** + * The incoming queue of messages to act on. + */ + private BlockingQueue incomingMessages; + + + /** + * The heart beat service we are acting on. + */ + private IncomingHeartBeatService incomingHeartBeatService; + + + /** + * The queue to place commands on. + */ + private BlockingQueue outgoingCommands; + + + + /** + * Constructs a {@link IncomingHeartBeatService} controller with the given parameters. + * This accepts connection related messages, converts them to commands, and passes them to an outgoing command queue. + * @param incomingMessages The message queue to read from. + * @param incomingHeartBeatService The IncomingHeartBeatService (context) to act on. + * @param outgoingCommands The queue to place outgoing commands on. + */ + public IncomingHeartBeatController(BlockingQueue incomingMessages, IncomingHeartBeatService incomingHeartBeatService, BlockingQueue outgoingCommands) { + this.incomingMessages = incomingMessages; + this.incomingHeartBeatService = incomingHeartBeatService; + this.outgoingCommands = outgoingCommands; + } + + + + @Override + public void run() { + + while (!Thread.interrupted()) { + + try { + AC35Data message = incomingMessages.take(); + Command command = IncomingHeartBeatCommandFactory.create(message, incomingHeartBeatService); + outgoingCommands.put(command); + + } catch (CommandConstructionException e) { + Logger.getGlobal().log(Level.WARNING, "IncomingHeartBeatController: " + this + " could not create command from message.", e); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "IncomingHeartBeatController: " + this + " was interrupted on thread: " + Thread.currentThread(), e); + Thread.currentThread().interrupt(); + + } + + + } + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java new file mode 100644 index 00000000..e7c5b159 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java @@ -0,0 +1,109 @@ +package visualiser.network; + +import mock.model.commandFactory.Command; +import shared.model.RunnableWithFramePeriod; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Tracks the heart beat status of a connection. + */ +public class IncomingHeartBeatService implements RunnableWithFramePeriod { + + + /** + * Timestamp of the last sent heartbeat message. + */ + private long lastHeartbeatTime; + + + /** + * Sequence number for heartbeat messages. + */ + private long lastHeartBeatSeqNum; + + + /** + * The incoming commands to execute. + */ + private BlockingQueue incomingCommands; + + + + /** + * Creates an {@link IncomingHeartBeatService} which executes commands from a given queue. + * @param incomingCommands Queue to read and execute commands from. + */ + public IncomingHeartBeatService(BlockingQueue incomingCommands) { + this.incomingCommands = incomingCommands; + + this.lastHeartbeatTime = System.currentTimeMillis(); + this.lastHeartBeatSeqNum = -1; + } + + + /** + * Sets the last heart beat time to a given value. + * @param lastHeartbeatTime Timestamp of heartbeat. + */ + public void setLastHeartbeatTime(long lastHeartbeatTime) { + this.lastHeartbeatTime = lastHeartbeatTime; + } + + /** + * Sets the last heart beat sequence number to a given value. + * @param lastHeartBeatSeqNum Sequence number of heartbeat. + */ + public void setLastHeartBeatSeqNum(long lastHeartBeatSeqNum) { + this.lastHeartBeatSeqNum = lastHeartBeatSeqNum; + } + + + + /** + * Calculates the time since last heartbeat, in milliseconds. + * + * @return Time since last heartbeat, in milliseconds.. + */ + private long timeSinceHeartbeat() { + long now = System.currentTimeMillis(); + return (now - lastHeartbeatTime); + } + + + /** + * Returns whether or not the heartBeat service considers the connection "alive". + * Going 10,000ms without receiving a heartBeat means that the connection is "dead". + * @return True if alive, false if dead. + */ + public boolean isAlive() { + long heartBeatPeriod = 10000; + + return (timeSinceHeartbeat() < heartBeatPeriod); + } + + + + @Override + public void run() { + + + while (!Thread.interrupted()) { + + try { + Command command = incomingCommands.take(); + command.execute(); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "IncomingHeartBeatService: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading command.", e); + Thread.currentThread().interrupt(); + + } + + } + + } +} diff --git a/racevisionGame/src/main/java/visualiser/network/ServerConnection.java b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java new file mode 100644 index 00000000..35b25727 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java @@ -0,0 +1,459 @@ +package visualiser.network; + + +import mock.model.commandFactory.Command; +import network.MessageRouters.MessageRouter; +import network.Messages.AC35Data; +import network.Messages.Enums.MessageType; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import network.Messages.LatestMessages; +import network.StreamRelated.MessageDeserialiser; +import network.StreamRelated.MessageSerialiser; +import shared.model.RunnableWithFramePeriod; +import visualiser.model.VisualiserRaceController; +import visualiser.enums.ConnectionToServerState; +import visualiser.gameController.ControllerClient; + +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class handles the client-server connection handshake, and creation of VisualiserInput and ControllerClient. + */ +public class ServerConnection implements RunnableWithFramePeriod { + + /** + * The socket for the connection to server. + */ + 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. + */ + private LatestMessages latestMessages; + + + /** + * Used to send client input to server. + */ + private ControllerClient controllerClient; + + + + /** + * Used to write messages to socket. + */ + private MessageSerialiser messageSerialiser; + /** + * The thread {@link #messageSerialiser} runs on. + */ + private Thread messageSerialiserThread; + + /** + * Used to read messages from socket. + */ + private MessageDeserialiser messageDeserialiser; + /** + * The thread {@link #messageDeserialiser} runs on. + */ + private Thread messageDeserialiserThread; + + + + + /** + * Router to route messages to correct queue. + */ + private MessageRouter messageRouter; + /** + * The thread {@link #messageRouter} runs on. + */ + private Thread messageRouterThread; + + + + /** + * The state of the connection to the client. + */ + private ConnectionToServer connectionToServer; + /** + * The thread {@link #connectionToServer} runs on. + */ + private Thread connectionToServerThread; + + /** + * The controller which handles JoinAcceptance messages. + */ + private ConnectionToServerController connectionToServerController; + /** + * The thread {@link #connectionToServerController} runs on. + */ + private Thread connectionToServerControllerThread; + + + + /** + * Tracks the heartBeat status of the connection. + */ + private IncomingHeartBeatService heartBeatService; + /** + * The thread {@link #heartBeatService} runs on. + */ + private Thread heartBeatServiceThread; + + /** + * Tracks the heartBeat status of the connection. + */ + private IncomingHeartBeatController heartBeatController; + /** + * The thread {@link #heartBeatController} runs on. + */ + private Thread heartBeatControllerThread; + + + + /** + * Used to convert incoming messages into a race snapshot. + */ + private VisualiserRaceController visualiserRaceController; + /** + * The thread {@link #visualiserRaceController} runs on. + */ + private Thread visualiserInputThread; + + + + + /** + * Creates a server connection, using a given socket. + * @param socket The socket which connects to the client. + * @param latestMessages Latest race snapshot to send to client. + * @param requestType The type of join request to make. + * @throws IOException Thrown if there is a problem with the client socket. + */ + public ServerConnection(Socket socket, LatestMessages latestMessages, RequestToJoinEnum requestType) throws IOException { + this.socket = socket; + this.latestMessages = latestMessages; + + createMessageSerialiser(socket); + createMessageDeserialiser(socket); + + createRouter(messageDeserialiser.getMessagesRead()); + + createConnectionToServer(requestType); + + + messageRouterThread.start(); + + + this.controllerClient = new ControllerClient(messageRouter.getIncomingMessageQueue()); + + } + + + + /** + * Creates this connection's {@link MessageRouter}, and gives it a queue to read from. + * Does not start {@link #messageRouterThread}. Start it after setting up any initial routes. + * @param inputQueue Queue for the MessageRouter to read from. + */ + private void createRouter(BlockingQueue inputQueue) { + this.messageRouter = new MessageRouter(inputQueue); + + this.messageRouterThread = new Thread(messageRouter, "ServerConnection()->MessageRouter thread " + messageRouter); + + //Unrouted messages get sent back to the router. Kind of ugly, but we do this to ensure that no messages are lost while initializing (e.g., XML message being received before setting up the route for it). + messageRouter.addDefaultRoute(messageRouter.getIncomingMessageQueue()); + } + + + /** + * Creates the {@link #connectionToServer} and {@link #connectionToServerController}, and starts their threads. + * @param requestType The type of join request to make to server. + */ + private void createConnectionToServer(RequestToJoinEnum requestType) { + + //ConnectionToServer executes these commands. + BlockingQueue commands = new LinkedBlockingQueue<>(); + this.connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, requestType, commands, messageRouter.getIncomingMessageQueue()); + + //ConnectionToServerController receives messages, and places commands on the above command queue. + BlockingQueue incomingJoinMessages = new LinkedBlockingQueue<>(); + this.connectionToServerController = new ConnectionToServerController(incomingJoinMessages, connectionToServer, commands); + + //Route JoinAcceptance messages to the controller, and RequestToJoin to the socket. + this.messageRouter.addRoute(MessageType.JOIN_ACCEPTANCE, incomingJoinMessages); + this.messageRouter.addRoute(MessageType.REQUEST_TO_JOIN, messageSerialiser.getMessagesToSend()); + + + //Start the above on new threads. + this.connectionToServerThread = new Thread(connectionToServer, "ServerConnection()->ConnectionToServer thread " + connectionToServer); + this.connectionToServerThread.start(); + + this.connectionToServerControllerThread = new Thread(connectionToServerController,"ServerConnection()->ConnectionToServerController thread " + connectionToServerController); + this.connectionToServerControllerThread.start(); + } + + + /** + * Creates the {@link #messageSerialiser} and starts its thread. + * @param socket The socket to write to. + * @throws IOException Thrown if we cannot get an outputStream from the socket + */ + private void createMessageSerialiser(Socket socket) throws IOException { + BlockingQueue outputQueue = new LinkedBlockingQueue<>(); + this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue); + + this.messageSerialiserThread = new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser); + this.messageSerialiserThread.start(); + } + + /** + * Creates the {@link #messageDeserialiser} and starts its thread. + * @param socket The socket to read from. + * @throws IOException Thrown if we cannot get an inputStream from the socket + */ + private void createMessageDeserialiser(Socket socket) throws IOException { + BlockingQueue inputQueue = new LinkedBlockingQueue<>(); + this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue); + + this.messageDeserialiserThread = new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser); + this.messageDeserialiserThread.start(); + } + + + /** + * Creates the {@link #heartBeatService} and {@link #heartBeatController} and starts its thread. + */ + private void createHeartBeatService() { + + //IncomingHeartBeatService executes these commands. + BlockingQueue commands = new LinkedBlockingQueue<>(); + this.heartBeatService = new IncomingHeartBeatService(commands); + + //IncomingHeartBeatController receives messages, and places commands on the above command queue. + BlockingQueue incomingHeartBeatMessages = new LinkedBlockingQueue<>(); + this.heartBeatController = new IncomingHeartBeatController(incomingHeartBeatMessages, heartBeatService, commands); + + //Route HeartBeat messages to the controller. + this.messageRouter.addRoute(MessageType.HEARTBEAT, incomingHeartBeatMessages); + + + //Start the above on new threads. + this.heartBeatServiceThread = new Thread(heartBeatService, "ServerConnection()->IncomingHeartBeatService thread " + connectionToServer); + this.heartBeatServiceThread.start(); + + this.heartBeatControllerThread = new Thread(heartBeatController,"ServerConnection()->IncomingHeartBeatController thread " + connectionToServerController); + this.heartBeatControllerThread.start(); + + } + + + + private void createVisualiserRace() { + + BlockingQueue incomingMessages = new LinkedBlockingQueue<>(); + + this.visualiserRaceController = new VisualiserRaceController(latestMessages, incomingMessages); + this.visualiserInputThread = new Thread(visualiserRaceController, "ServerConnection()->VisualiserInput thread " + visualiserRaceController); + this.visualiserInputThread.start(); + + //Routes. + this.messageRouter.addRoute(MessageType.BOATLOCATION, incomingMessages); + this.messageRouter.addRoute(MessageType.RACESTATUS, incomingMessages); + this.messageRouter.addRoute(MessageType.RACESTARTSTATUS, incomingMessages); + this.messageRouter.addRoute(MessageType.AVGWIND, incomingMessages); + this.messageRouter.addRoute(MessageType.COURSEWIND, incomingMessages); + this.messageRouter.addRoute(MessageType.CHATTERTEXT, incomingMessages); + this.messageRouter.addRoute(MessageType.DISPLAYTEXTMESSAGE, incomingMessages); + this.messageRouter.addRoute(MessageType.YACHTACTIONCODE, incomingMessages); + this.messageRouter.addRoute(MessageType.YACHTEVENTCODE, incomingMessages); + this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages); + this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages); + this.messageRouter.removeDefaultRoute(); + + //TODO create VisualiserRace here or somewhere else? + + + } + + + + private void createPlayerInputController() { + + this.messageRouter.addRoute(MessageType.BOATACTION, messageSerialiser.getMessagesToSend()); + //TODO routes + } + + + + + @Override + public void run() { + + //Monitor the connection state. + + long previousFrameTime = System.currentTimeMillis(); + + while (!Thread.interrupted()) { + + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentFrameTime, 100); + previousFrameTime = currentFrameTime; + + + ConnectionToServerState state = connectionToServer.getConnectionState(); + + switch (state) { + + case CONNECTED: + connected(); + break; + + case DECLINED: + declined(); + break; + + case TIMED_OUT: + timedOut(); + break; + + } + + } + + } + + + + /** + * Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#CONNECTED}. + */ + private void connected() { + + JoinAcceptance joinAcceptance = connectionToServer.getJoinAcceptance(); + + allocatedSourceID = joinAcceptance.getSourceID(); + + + createHeartBeatService(); + + createVisualiserRace(); + + createPlayerInputController(); + + + //We interrupt as this thread's run() isn't needed anymore. + Thread.currentThread().interrupt(); + } + + + /** + * Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#DECLINED}. + */ + private void declined() { + Logger.getGlobal().log(Level.WARNING, "Server handshake failed. Connection was declined."); + + terminate(); + + Thread.currentThread().interrupt(); + } + + + /** + * Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#TIMED_OUT}. + */ + private void timedOut() { + Logger.getGlobal().log(Level.WARNING, "Server handshake failed. Connection timed out."); + + terminate(); + + Thread.currentThread().interrupt(); + } + + + + + /** + * Determines whether or not this connection is still alive. + * This is based off whether the {@link #messageDeserialiser}, {@link #messageSerialiser}, and {@link #heartBeatService} are alive. + * @return True if it is alive, false otherwise. + */ + public boolean isAlive() { + return messageDeserialiser.isRunning() && messageSerialiser.isRunning() && heartBeatService.isAlive(); + } + + + /** + * 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; + } + + + /** + * Terminates the connection and any running threads. + */ + public void terminate() { + + if (this.messageRouterThread != null) { + this.messageRouterThread.interrupt(); + } + + + if (this.messageSerialiserThread != null) { + this.messageSerialiserThread.interrupt(); + } + if (this.messageDeserialiserThread != null) { + this.messageDeserialiserThread.interrupt(); + } + + + + if (this.connectionToServerThread != null) { + this.connectionToServerThread.interrupt(); + } + if (this.connectionToServerControllerThread != null) { + this.connectionToServerControllerThread.interrupt(); + } + + + if (this.heartBeatServiceThread != null) { + this.heartBeatServiceThread.interrupt(); + } + if (this.heartBeatControllerThread != null) { + this.heartBeatControllerThread.interrupt(); + } + + + if (this.visualiserInputThread != null) { + this.visualiserInputThread.interrupt(); + } + //TODO visualiser race? + //TODO input controller? + + } + + +} diff --git a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java new file mode 100644 index 00000000..fae94d1b --- /dev/null +++ b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java @@ -0,0 +1,150 @@ +package visualiser.network; + +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.Enums.JoinAcceptanceEnum; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import org.junit.Before; +import org.junit.Test; +import visualiser.Commands.ConnectionToServerCommands.JoinSuccessfulCommand; +import visualiser.Commands.ConnectionToServerCommands.RaceParticipantsFullCommand; +import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand; +import visualiser.enums.ConnectionToServerState; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.Assert.*; + +/** + * Tests the {@link ConnectionToServer} class with a Participant request, and how it reacts to various commands. + */ +public class ConnectionToServerParticipantTest { + + private ConnectionToServer connectionToServer; + private Thread connectionToServerThread; + + private BlockingQueue outgoingMessages; + private BlockingQueue incomingCommands; + + + @Before + public void setUp() throws Exception { + + incomingCommands = new LinkedBlockingQueue<>(); + outgoingMessages = new LinkedBlockingQueue<>(); + + connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, RequestToJoinEnum.PARTICIPANT, incomingCommands, outgoingMessages); + connectionToServerThread = new Thread(connectionToServer); + connectionToServerThread.start(); + + } + + + /** + * When a connection to server is created, is it expected that it will have sent a request and be in the Request_sent state. + * @throws Exception On error. + */ + @Test + public void expectRequestSent() throws Exception { + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState()); + } + + + /** + * When the connection to server thread is interrupted, it is expected the connection state will become TimedOut. + * @throws Exception On error. + */ + @Test + public void interruptTimedOut() throws Exception { + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + connectionToServerThread.interrupt(); + connectionToServerThread.join(); + + assertEquals(ConnectionToServerState.TIMED_OUT, connectionToServer.getConnectionState()); + } + + + /** + * Sends a join successful command. Expects that the connection becomes Connected. + * @throws Exception On error. + */ + @Test + public void sendJoinSuccessCommand() throws Exception { + int sourceID = 123; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID); + + Command command = new JoinSuccessfulCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertNotEquals(0, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + + + } + + + /** + * Sends a participants full command. Expects that the connection becomes Declined. + * @throws Exception On error. + */ + @Test + public void sendRaceParticipantsFullCommand() throws Exception { + int sourceID = 0; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, sourceID); + + Command command = new RaceParticipantsFullCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + } + + + /** + * Sends a server full command. Expects that the connection becomes Declined. + * @throws Exception On error. + */ + @Test + public void sendServerFullCommand() throws Exception { + int sourceID = 0; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.SERVER_FULL, sourceID); + + Command command = new ServerFullCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.SERVER_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + } + + +} + + diff --git a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java new file mode 100644 index 00000000..8b69c861 --- /dev/null +++ b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java @@ -0,0 +1,109 @@ +package visualiser.network; + +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.Enums.JoinAcceptanceEnum; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import org.junit.Before; +import org.junit.Test; +import visualiser.Commands.ConnectionToServerCommands.JoinSuccessfulCommand; +import visualiser.Commands.ConnectionToServerCommands.RaceParticipantsFullCommand; +import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand; +import visualiser.enums.ConnectionToServerState; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.Assert.*; + +/** + * Tests the {@link ConnectionToServer} class with a Spectator request, and how it reacts to various commands. + */ +public class ConnectionToServerSpectatorTest { + + private ConnectionToServer connectionToServer; + private Thread connectionToServerThread; + + private BlockingQueue outgoingMessages; + private BlockingQueue incomingCommands; + + + @Before + public void setUp() throws Exception { + + incomingCommands = new LinkedBlockingQueue<>(); + outgoingMessages = new LinkedBlockingQueue<>(); + + connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, RequestToJoinEnum.SPECTATOR, incomingCommands, outgoingMessages); + connectionToServerThread = new Thread(connectionToServer); + connectionToServerThread.start(); + + } + + + /** + * When a connection to server is created, is it expected that it will have sent a request and be in the Request_sent state. + * @throws Exception On error. + */ + @Test + public void expectRequestSent() throws Exception { + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState()); + } + + + /** + * Sends a join successful command. Expects that the connection becomes Connected. + * @throws Exception On error. + */ + @Test + public void sendJoinSuccessCommand() throws Exception { + int sourceID = 0; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID); + + Command command = new JoinSuccessfulCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + + + } + + + /** + * Sends a server full command. Expects that the connection becomes Declined. + * @throws Exception On error. + */ + @Test + public void sendServerFullCommand() throws Exception { + int sourceID = 0; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.SERVER_FULL, sourceID); + + Command command = new ServerFullCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.SERVER_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + } + + +} + + From 7366aba5ec769fae5b3411b9376d090c3273664d Mon Sep 17 00:00:00 2001 From: fjc40 Date: Mon, 14 Aug 2017 00:53:05 +1200 Subject: [PATCH 07/16] Added empty data sources, to be used by VisualiserRace. Created FrameRateTracker which can be used to track framerate. Updated RequestToJoinEnum and JoinAcceptanceEnum to match the agreed connection API. Added AssignPlayerBoat message, which is used internally on the client to assign the player a source ID once they have connected. Fixed some race conditions in the MessageRouter. Updated ConnectionAcceptor.CheckClientConnection to wait slightly longer before removing connection (there was a slight race condition before). Race no longer has a reference to LatestMessages. LatestMessages no longer has specific messages types in it. Created RaceState class, which contains the state that is shared between VisualiserRaceState and MockRaceState (currently only used on visualiser). Split VisualiserRace into VisualiserRaceState and VisualiserRaceService. Added the VisualiserRace commands (BoatLocatonCommand, RaceStatusCommand, etc...). Slightly increased the preferred width of race.fxml table columns. issues #27 #37 #35 #story[1095] --- .../java/mock/app/ConnectionAcceptor.java | 33 +- .../src/main/java/mock/app/Event.java | 1 - .../java/mock/model/ClientConnection.java | 2 +- .../src/main/java/mock/model/MockRace.java | 9 +- .../network/MessageRouters/MessageRouter.java | 14 +- .../network/Messages/AssignPlayerBoat.java | 39 ++ .../java/network/Messages/BoatStatus.java | 4 +- .../Messages/Enums/JoinAcceptanceEnum.java | 25 +- .../network/Messages/Enums/MessageType.java | 14 +- .../Messages/Enums/RequestToJoinEnum.java | 7 +- .../java/network/Messages/LatestMessages.java | 162 ------ .../shared/dataInput/EmptyBoatDataSource.java | 47 ++ .../shared/dataInput/EmptyRaceDataSource.java | 129 +++++ .../dataInput/EmptyRegattaDataSource.java | 122 +++++ .../exceptions/MarkNotFoundException.java | 15 + .../java/shared/model/FrameRateTracker.java | 109 ++++ .../src/main/java/shared/model/Race.java | 34 +- .../src/main/java/shared/model/RaceState.java | 346 +++++++++++++ .../shared/model/RunnableWithFramePeriod.java | 1 + .../ConnectionToServerCommandFactory.java | 8 +- ...llCommand.java => JoinFailureCommand.java} | 8 +- .../JoinSuccessParticipantCommand.java | 57 +++ ....java => JoinSuccessSpectatorCommand.java} | 10 +- .../AssignPlayerBoatCommand.java | 53 ++ .../BoatLocationCommand.java | 127 +++++ .../BoatsXMLMessageCommand.java | 48 ++ .../RaceStatusCommand.java | 185 +++++++ .../RaceXMLMessageCommand.java | 44 ++ .../RegattaXMLMessageCommand.java | 44 ++ .../VisualiserRaceCommandFactory.java | 40 ++ .../XMLMessageCommandFactory.java | 63 +++ .../Controllers/ArrowController.java | 2 - .../Controllers/MainController.java | 9 +- .../Controllers/RaceController.java | 68 ++- .../Controllers/StartController.java | 133 ++--- .../main/java/visualiser/model/RaceMap.java | 31 +- .../visualiser/model/ResizableRaceCanvas.java | 30 +- .../main/java/visualiser/model/Sparkline.java | 7 +- .../java/visualiser/model/VisualiserBoat.java | 37 +- .../java/visualiser/model/VisualiserRace.java | 472 ------------------ .../model/VisualiserRaceController.java | 192 ++----- .../visualiser/model/VisualiserRaceEvent.java | 119 +++++ .../model/VisualiserRaceService.java | 93 ++++ .../visualiser/model/VisualiserRaceState.java | 411 +++++++++++++++ .../network/ConnectionToServer.java | 18 +- .../visualiser/network/ServerConnection.java | 94 ++-- .../main/resources/mock/mockXML/boatTest.xml | 2 +- .../resources/visualiser/scenes/race.fxml | 24 +- .../test/java/mock/model/MockRaceTest.java | 3 +- .../model/commandFactory/WindCommandTest.java | 5 - .../JoinAcceptanceDecoderTest.java | 21 +- .../ConnectionToServerParticipantTest.java | 35 +- .../ConnectionToServerSpectatorTest.java | 15 +- 53 files changed, 2495 insertions(+), 1126 deletions(-) create mode 100644 racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java create mode 100644 racevisionGame/src/main/java/shared/dataInput/EmptyBoatDataSource.java create mode 100644 racevisionGame/src/main/java/shared/dataInput/EmptyRaceDataSource.java create mode 100644 racevisionGame/src/main/java/shared/dataInput/EmptyRegattaDataSource.java create mode 100644 racevisionGame/src/main/java/shared/exceptions/MarkNotFoundException.java create mode 100644 racevisionGame/src/main/java/shared/model/FrameRateTracker.java create mode 100644 racevisionGame/src/main/java/shared/model/RaceState.java rename racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/{RaceParticipantsFullCommand.java => JoinFailureCommand.java} (76%) create mode 100644 racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessParticipantCommand.java rename racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/{JoinSuccessfulCommand.java => JoinSuccessSpectatorCommand.java} (71%) create mode 100644 racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/AssignPlayerBoatCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatLocationCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatsXMLMessageCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceXMLMessageCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RegattaXMLMessageCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/XMLMessageCommandFactory.java delete mode 100644 racevisionGame/src/main/java/visualiser/model/VisualiserRace.java create mode 100644 racevisionGame/src/main/java/visualiser/model/VisualiserRaceEvent.java create mode 100644 racevisionGame/src/main/java/visualiser/model/VisualiserRaceService.java create mode 100644 racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java diff --git a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java index ba22fef0..d399210d 100644 --- a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java +++ b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java @@ -13,7 +13,12 @@ import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; @@ -36,7 +41,7 @@ public class ConnectionAcceptor implements Runnable { /** * List of client connections. */ - private ArrayBlockingQueue clientConnections = new ArrayBlockingQueue<>(16, true); + private BlockingQueue clientConnections = new ArrayBlockingQueue<>(16, true); /** * Snapshot of the race. @@ -137,13 +142,13 @@ public class ConnectionAcceptor implements Runnable { */ class CheckClientConnection implements Runnable{ - private ArrayBlockingQueue connections; + private BlockingQueue connections; /** * Constructor * @param connections Clients "connected" */ - public CheckClientConnection(ArrayBlockingQueue connections){ + public CheckClientConnection(BlockingQueue connections){ this.connections = connections; } @@ -153,13 +158,29 @@ public class ConnectionAcceptor implements Runnable { @Override public void run() { - while(true) { - //System.out.println(connections.size());//used to see current amount of visualisers connected. - ArrayBlockingQueue clientConnections = new ArrayBlockingQueue<>(16, true, connections); + //We track the number of times each connection fails the !isAlive() test. + //This is to give a bit of lee-way in case the connection checker checks a connection before its thread has actually started. + Map connectionDeadCount = new HashMap<>(); + + while(!Thread.interrupted()) { + + //Make copy of connections. + List clientConnections = new ArrayList<>(connections); + for (ClientConnection client : clientConnections) { + + connectionDeadCount.put(client, connectionDeadCount.getOrDefault(client, 0)); + if (!client.isAlive()) { + //Add one to fail count. + connectionDeadCount.put(client, connectionDeadCount.get(client) + 1); + } + + //We only remove them if they fail 5 times. + if (connectionDeadCount.get(client) > 5) { connections.remove(client); + connectionDeadCount.remove(client); client.terminate(); Logger.getGlobal().log(Level.WARNING, "CheckClientConnection is removing the dead connection: " + client); diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index b5b9865f..f0783fc0 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -127,7 +127,6 @@ public class Event { boatDataSource, raceDataSource, regattaDataSource, - this.latestMessages, this.boatPolars, Constants.RaceTimeScale, windGenerator ), diff --git a/racevisionGame/src/main/java/mock/model/ClientConnection.java b/racevisionGame/src/main/java/mock/model/ClientConnection.java index 23e00819..d2ca7609 100644 --- a/racevisionGame/src/main/java/mock/model/ClientConnection.java +++ b/racevisionGame/src/main/java/mock/model/ClientConnection.java @@ -245,7 +245,7 @@ public class ClientConnection implements Runnable { private void sendJoinAcceptanceMessage(int sourceID) throws HandshakeException { //Send them the source ID. - JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID); + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, sourceID); try { outputQueue.put(joinAcceptance); diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 2f23712f..3d895478 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -53,14 +53,13 @@ public class MockRace extends Race { * @param boatDataSource Data source for boat related data (yachts and marker boats). * @param raceDataSource Data source for race related data (participating boats, legs, etc...). * @param regattaDataSource Data source for race related data (course name, location, timezone, etc...). - * @param latestMessages The LatestMessages to send events to. * @param polars The polars table to be used for boat simulation. * @param timeScale The timeScale for the race. See {@link Constants#RaceTimeScale}. * @param windGenerator The wind generator used for the race. */ - public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale, WindGenerator windGenerator) { + public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, Polars polars, int timeScale, WindGenerator windGenerator) { - super(boatDataSource, raceDataSource, regattaDataSource, latestMessages); + super(boatDataSource, raceDataSource, regattaDataSource); this.scaleFactor = timeScale; @@ -456,7 +455,5 @@ public class MockRace extends Race { } - public List getCompoundMarks() { - return compoundMarks; - } + } diff --git a/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java index b767b880..5b5bcbf5 100644 --- a/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java +++ b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java @@ -103,15 +103,19 @@ public class MessageRouter implements RunnableWithFramePeriod { AC35Data message = incomingMessages.take(); - if (routeMap.containsKey(message.getType())) { - //We have a route. - routeMap.get(message.getType()).put(message); + BlockingQueue queue = routeMap.get(message.getType()); + + if (queue != null) { + queue.put(message); } else { //No route. Use default. - if (defaultRoute.isPresent()) { - defaultRoute.get().put(message); + BlockingQueue defaultQueue = defaultRoute.orElse(null); + + if (defaultQueue != null) { + defaultQueue.put(message); } + } diff --git a/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java b/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java new file mode 100644 index 00000000..c4197fe9 --- /dev/null +++ b/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java @@ -0,0 +1,39 @@ +package network.Messages; + +import network.Messages.Enums.JoinAcceptanceEnum; +import network.Messages.Enums.MessageType; + + +/** + * This is the message the client generates and sends to itself once the server has assigned a boat source ID with {@link JoinAcceptance}. + */ +public class AssignPlayerBoat extends AC35Data { + + + /** + * The source ID of the boat assigned to the client. + * 0 indicates they haven't been assigned a boat. + */ + private int sourceID = 0; + + + + + /** + * Constructs a AssignPlayerBoat message. + * @param sourceID The sourceID to assign to the client. 0 indicates no sourceID. + */ + public AssignPlayerBoat(int sourceID){ + super(MessageType.ASSIGN_PLAYER_BOAT); + this.sourceID = sourceID; + } + + + /** + * Returns the source ID of the boat assigned to the client. + * @return The source ID of the boat assigned to the client. + */ + public int getSourceID() { + return sourceID; + } +} diff --git a/racevisionGame/src/main/java/network/Messages/BoatStatus.java b/racevisionGame/src/main/java/network/Messages/BoatStatus.java index b62c4469..7fa9228f 100644 --- a/racevisionGame/src/main/java/network/Messages/BoatStatus.java +++ b/racevisionGame/src/main/java/network/Messages/BoatStatus.java @@ -128,7 +128,7 @@ public class BoatStatus { } /** - * Returns he time at which it is estimated the boat will reach the next mark. Milliseconds since unix epoch. + * Returns the time at which it is estimated the boat will reach the next mark. Milliseconds since unix epoch. * @return Time at which boat will reach next mark. */ public long getEstTimeAtNextMark() { @@ -136,7 +136,7 @@ public class BoatStatus { } /** - * Returns he time at which it is estimated the boat will finish the race. Milliseconds since unix epoch. + * Returns the time at which it is estimated the boat will finish the race. Milliseconds since unix epoch. * @return Time at which boat will finish the race. */ public long getEstTimeAtFinish() { diff --git a/racevisionGame/src/main/java/network/Messages/Enums/JoinAcceptanceEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/JoinAcceptanceEnum.java index 6939d8f3..908efa27 100644 --- a/racevisionGame/src/main/java/network/Messages/Enums/JoinAcceptanceEnum.java +++ b/racevisionGame/src/main/java/network/Messages/Enums/JoinAcceptanceEnum.java @@ -11,24 +11,35 @@ public enum JoinAcceptanceEnum { /** - * Client is allowed to join. + * Client is allowed to join and spectate. */ - JOIN_SUCCESSFUL(1), + JOIN_SUCCESSFUL_SPECTATOR(0), /** - * The race is full - no more participants allowed. + * Client is allowed to join and participate. */ - RACE_PARTICIPANTS_FULL(2), + JOIN_SUCCESSFUL_PARTICIPANT(1), /** - * The race cannot allow any more ghost participants to join. + * Client is allowed to join and play the tutorial. */ - GHOST_PARTICIPANTS_FULL(3), + JOIN_SUCCESSFUL_TUTORIAL(2), + + /** + * Client is allowed to join and participate as a ghost player. + */ + JOIN_SUCCESSFUL_GHOST(3), + + + /** + * Join Request was denied. + */ + JOIN_FAILURE(0x10), /** * The server is completely full, cannot participate or spectate. */ - SERVER_FULL(4), + SERVER_FULL(0x11), /** diff --git a/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java b/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java index 15f70f40..aed5d70a 100644 --- a/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java +++ b/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java @@ -20,17 +20,25 @@ public enum MessageType { COURSEWIND(44), AVGWIND(47), + + BOATACTION(100), + /** * This is used for {@link network.Messages.RequestToJoin} messages. */ - REQUEST_TO_JOIN(55), + REQUEST_TO_JOIN(101), /** * This is used for {@link network.Messages.JoinAcceptance} messages. */ - JOIN_ACCEPTANCE(56), + JOIN_ACCEPTANCE(102), + + + /** + * This is used for {@link network.Messages.AssignPlayerBoat} messages. + */ + ASSIGN_PLAYER_BOAT(121), - BOATACTION(100), NOTAMESSAGE(0); diff --git a/racevisionGame/src/main/java/network/Messages/Enums/RequestToJoinEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/RequestToJoinEnum.java index 36bb5955..d868a25d 100644 --- a/racevisionGame/src/main/java/network/Messages/Enums/RequestToJoinEnum.java +++ b/racevisionGame/src/main/java/network/Messages/Enums/RequestToJoinEnum.java @@ -20,10 +20,15 @@ public enum RequestToJoinEnum { */ PARTICIPANT(1), + /** + * Client wants to play the tutorial. + */ + CONTROL_TUTORIAL(2), + /** * Client wants to particpate as a ghost. */ - GHOST(5), + GHOST(3), /** diff --git a/racevisionGame/src/main/java/network/Messages/LatestMessages.java b/racevisionGame/src/main/java/network/Messages/LatestMessages.java index 147f58e7..738b3ae6 100644 --- a/racevisionGame/src/main/java/network/Messages/LatestMessages.java +++ b/racevisionGame/src/main/java/network/Messages/LatestMessages.java @@ -1,7 +1,6 @@ package network.Messages; import network.Messages.Enums.XMLMessageType; -import shared.dataInput.RaceDataSource; import java.util.*; @@ -11,36 +10,6 @@ import java.util.*; */ public class LatestMessages extends Observable { - /** - * The latest RaceStatus message. - */ - private RaceStatus raceStatus; - - /** - * A map of the last BoatStatus message received, for each boat. - */ - private final Map boatStatusMap = new HashMap<>(); - - /** - * A map of the last BoatLocation message received, for each boat. - */ - private final Map boatLocationMap = new HashMap<>(); - - /** - * A map of the last MarkRounding message received, for each boat. - */ - private final Map markRoundingMap = new HashMap<>(); - - /** - * The last AverageWind message received. - */ - private AverageWind averageWind; - - /** - * The last CourseWinds message received. - */ - private CourseWinds courseWinds; - /** * A list of messages containing a snapshot of the race. @@ -91,138 +60,7 @@ public class LatestMessages extends Observable { } - /** - * Gets the latest RaceStatus message received. - * @return The latest RaceStatus message received. - */ - public RaceStatus getRaceStatus() { - return raceStatus; - } - - /** - * Sets the latest RaceStatus message received. - * @param raceStatus The new RaceStatus message to store. - */ - public void setRaceStatus(RaceStatus raceStatus) { - this.raceStatus = raceStatus; - } - - - - /** - * Returns the latest BoatStatus message received for a given boat. - * @param sourceID Source ID of the boat. - * @return The latest BoatStatus message for the specified boat. - */ - public BoatStatus getBoatStatus(int sourceID) { - return boatStatusMap.get(sourceID); - } - - /** - * Inserts a BoatStatus message for a given boat. - * @param boatStatus The BoatStatus message to set. - */ - public void setBoatStatus(BoatStatus boatStatus) { - boatStatusMap.put(boatStatus.getSourceID(), boatStatus); - } - - - - /** - * Returns the latest BoatLocation message received for a given boat. - * @param sourceID Source ID of the boat. - * @return The latest BoatLocation message for the specified boat. - */ - public BoatLocation getBoatLocation(int sourceID) { - return boatLocationMap.get(sourceID); - } - - /** - * Inserts a BoatLocation message for a given boat. - * @param boatLocation The BoatLocation message to set. - */ - public void setBoatLocation(BoatLocation boatLocation) { - //TODO should compare the sequence number of the new boatLocation with the existing boatLocation for this boat (if it exists), and use the newer one. - boatLocationMap.put(boatLocation.getSourceID(), boatLocation); - } - - /** - * Returns the latest MarkRounding message received for a given boat. - * @param sourceID Source ID of the boat. - * @return The latest MarkRounding message for the specified boat. - */ - public MarkRounding getMarkRounding(int sourceID) { - return markRoundingMap.get(sourceID); - } - - /** - * Inserts a MarkRounding message for a given boat. - * @param markRounding The MarkRounding message to set. - */ - public void setMarkRounding(MarkRounding markRounding) { - //TODO should compare the sequence number of the new markRounding with the existing boatLocation for this boat (if it exists), and use the newer one. - markRoundingMap.put(markRounding.getSourceID(), markRounding); - } - - - - /** - * Gets the latest AverageWind message received. - * @return The latest AverageWind message received. - */ - public AverageWind getAverageWind() { - return averageWind; - } - - /** - * Sets the latest AverageWind message received. - * @param averageWind The new AverageWind message to store. - */ - public void setAverageWind(AverageWind averageWind) { - this.averageWind = averageWind; - } - - - /** - * Gets the latest CourseWinds message received. - * @return The latest CourseWinds message received. - */ - public CourseWinds getCourseWinds() { - return courseWinds; - } - /** - * Sets the latest CourseWinds message received. - * @param courseWinds The new CourseWinds message to store. - */ - public void setCourseWinds(CourseWinds courseWinds) { - this.courseWinds = courseWinds; - } - - - /** - * Returns the map of boat sourceIDs to BoatLocation messages. - * @return Map between boat sourceID and BoatLocation. - */ - public Map getBoatLocationMap() { - return boatLocationMap; - } - - /** - * Returns the map of boat sourceIDs to BoatStatus messages. - * @return Map between boat sourceID and BoatStatus. - */ - public Map getBoatStatusMap() { - return boatStatusMap; - } - - /** - * Returns the map of boat sourceIDs to MarkRounding messages. - * @return Map between boat sourceID and MarkRounding. - */ - public Map getMarkRoundingMap() { - return markRoundingMap; - } diff --git a/racevisionGame/src/main/java/shared/dataInput/EmptyBoatDataSource.java b/racevisionGame/src/main/java/shared/dataInput/EmptyBoatDataSource.java new file mode 100644 index 00000000..1de4251e --- /dev/null +++ b/racevisionGame/src/main/java/shared/dataInput/EmptyBoatDataSource.java @@ -0,0 +1,47 @@ +package shared.dataInput; + +import shared.model.Boat; +import shared.model.Mark; + +import java.util.HashMap; +import java.util.Map; + +/** + * An empty {@link BoatDataSource}. Can be used to initialise a race with no data. + */ +public class EmptyBoatDataSource implements BoatDataSource { + + /** + * A map of source ID to boat for all boats in the race. + */ + private final Map boatMap = new HashMap<>(); + + /** + * A map of source ID to mark for all marks in the race. + */ + private final Map markerMap = new HashMap<>(); + + + + public EmptyBoatDataSource() { + } + + + /** + * Get the boats that are going to participate in this race + * @return Dictionary of boats that are to participate in this race indexed by SourceID + */ + @Override + public Map getBoats() { + return boatMap; + } + + /** + * Get the marker Boats that are participating in this race + * @return Dictionary of the Markers Boats that are in this race indexed by their Source ID. + */ + @Override + public Map getMarkerBoats() { + return markerMap; + } +} diff --git a/racevisionGame/src/main/java/shared/dataInput/EmptyRaceDataSource.java b/racevisionGame/src/main/java/shared/dataInput/EmptyRaceDataSource.java new file mode 100644 index 00000000..74ccbaf5 --- /dev/null +++ b/racevisionGame/src/main/java/shared/dataInput/EmptyRaceDataSource.java @@ -0,0 +1,129 @@ +package shared.dataInput; + +import network.Messages.Enums.RaceTypeEnum; +import shared.model.CompoundMark; +import shared.model.GPSCoordinate; +import shared.model.Leg; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An empty {@link RaceDataSource}. Can be used to initialise a race with no data. + */ +public class EmptyRaceDataSource implements RaceDataSource { + + + /** + * The GPS coordinate of the top left of the race boundary. + */ + private GPSCoordinate mapTopLeft = new GPSCoordinate(0, 0); + + /** + * The GPS coordinate of the bottom right of the race boundary. + */ + private GPSCoordinate mapBottomRight = new GPSCoordinate(0, 0); + + + /** + * A list of GPS coordinates that make up the boundary of the race. + */ + private final List boundary = new ArrayList<>(); + + /** + * A map between compoundMarkID and a CompoundMark for all CompoundMarks in a race. + */ + private final Map compoundMarkMap = new HashMap<>(); + + /** + * A list of boat sourceIDs participating in the race. + */ + private final List participants = new ArrayList<>(); + + /** + * A list of legs in the race. + */ + private final List legs = new ArrayList<>(); + + + /** + * The time that the race.xml file was created. + */ + private ZonedDateTime creationTimeDate = ZonedDateTime.now(); + + /** + * The time that the race should start at, if it hasn't been postponed. + */ + private ZonedDateTime raceStartTime = ZonedDateTime.now().plusMinutes(5); + + /** + * Whether or not the race has been postponed. + */ + private boolean postpone = false; + + + /** + * The ID number of the race. + */ + private int raceID = 0; + + /** + * The type of the race. + */ + private RaceTypeEnum raceType = RaceTypeEnum.NOT_A_RACE_TYPE; + + + + public EmptyRaceDataSource() { + } + + + + public List getBoundary() { + return boundary; + } + + public GPSCoordinate getMapTopLeft() { + return mapTopLeft; + } + + public GPSCoordinate getMapBottomRight() { + return mapBottomRight; + } + + public List getLegs() { + return legs; + } + + public List getCompoundMarks() { + return new ArrayList<>(compoundMarkMap.values()); + } + + + public ZonedDateTime getCreationDateTime() { + return creationTimeDate; + } + + public ZonedDateTime getStartDateTime() { + return raceStartTime; + } + + public int getRaceId() { + return raceID; + } + + public RaceTypeEnum getRaceType() { + return raceType; + } + + public boolean getPostponed() { + return postpone; + } + + public List getParticipants() { + return participants; + } +} diff --git a/racevisionGame/src/main/java/shared/dataInput/EmptyRegattaDataSource.java b/racevisionGame/src/main/java/shared/dataInput/EmptyRegattaDataSource.java new file mode 100644 index 00000000..05a0fcf9 --- /dev/null +++ b/racevisionGame/src/main/java/shared/dataInput/EmptyRegattaDataSource.java @@ -0,0 +1,122 @@ +package shared.dataInput; + +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import shared.enums.XMLFileType; +import shared.exceptions.InvalidRegattaDataException; +import shared.exceptions.XMLReaderException; +import shared.model.GPSCoordinate; + +import java.io.InputStream; + +/** + * An empty {@link RegattaDataSource}. Can be used to initialise a race with no data. + */ +public class EmptyRegattaDataSource implements RegattaDataSource { + /** + * The regatta ID. + */ + private int regattaID = 0; + + /** + * The regatta name. + */ + private String regattaName = ""; + + /** + * The race ID. + */ + private int raceID = 0; + + /** + * The course name. + */ + private String courseName = ""; + + /** + * The central latitude of the course. + */ + private double centralLatitude = 0; + + /** + * The central longitude of the course. + */ + private double centralLongitude = 0; + + /** + * The central altitude of the course. + */ + private double centralAltitude = 0; + + /** + * The UTC offset of the course. + */ + private float utcOffset = 0; + + /** + * The magnetic variation of the course. + */ + private float magneticVariation = 0; + + + + + public EmptyRegattaDataSource() { + } + + + + + public int getRegattaID() { + return regattaID; + } + + + public String getRegattaName() { + return regattaName; + } + + + public int getRaceID() { + return raceID; + } + + + public String getCourseName() { + return courseName; + } + + + public double getCentralLatitude() { + return centralLatitude; + } + + + public double getCentralLongitude() { + return centralLongitude; + } + + + public double getCentralAltitude() { + return centralAltitude; + } + + + public float getUtcOffset() { + return utcOffset; + } + + + public float getMagneticVariation() { + return magneticVariation; + } + + + /** + * Returns the GPS coorindates of the centre of the regatta. + * @return The gps coordinate for the centre of the regatta. + */ + public GPSCoordinate getGPSCoordinate() { + return new GPSCoordinate(centralLatitude, centralLongitude); + } +} diff --git a/racevisionGame/src/main/java/shared/exceptions/MarkNotFoundException.java b/racevisionGame/src/main/java/shared/exceptions/MarkNotFoundException.java new file mode 100644 index 00000000..49cf4f5c --- /dev/null +++ b/racevisionGame/src/main/java/shared/exceptions/MarkNotFoundException.java @@ -0,0 +1,15 @@ +package shared.exceptions; + +/** + * An exception thrown when a specific mark cannot be found. + */ +public class MarkNotFoundException extends Exception { + + public MarkNotFoundException(String message) { + super(message); + } + + public MarkNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/racevisionGame/src/main/java/shared/model/FrameRateTracker.java b/racevisionGame/src/main/java/shared/model/FrameRateTracker.java new file mode 100644 index 00000000..ba683f01 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/FrameRateTracker.java @@ -0,0 +1,109 @@ +package shared.model; + + +import javafx.animation.AnimationTimer; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; + + +/** + * This class is used to track the framerate of something. + * Use {@link #incrementFps(long)} to update it, and {@link #fpsProperty()} to observe it. + */ +public class FrameRateTracker { + + + /** + * The number of frames per second. + * We essentially track the number of frames generated per second, over a one second period. When {@link #lastFpsResetTime} reaches 1 second, {@link #currentFps} is reset. + */ + private int currentFps = 0; + + /** + * The number of frames per second we generated over the last 1 second period. + */ + private IntegerProperty lastFps = new SimpleIntegerProperty(0); + + /** + * The time, in milliseconds, since we last reset our {@link #currentFps} counter. + */ + private long lastFpsResetTime; + + + /** + * Creates a {@link FrameRateTracker}. Use {@link #incrementFps(long)} to update it, and {@link #fpsProperty()} to observe it. + */ + public FrameRateTracker() { + timer.start(); + } + + + /** + * Returns the number of frames generated per second. + * @return Frames per second. + */ + public int getFps() { + return lastFps.getValue(); + } + + /** + * Returns the fps property. + * @return The fps property. + */ + public IntegerProperty fpsProperty() { + return lastFps; + } + + + /** + * Increments the FPS counter, and adds timePeriod milliseconds to our FPS reset timer. + * @param timePeriod Time, in milliseconds, to add to {@link #lastFpsResetTime}. + */ + private void incrementFps(long timePeriod) { + //Increment. + this.currentFps++; + + //Add period to timer. + this.lastFpsResetTime += timePeriod; + + //If we have reached 1 second period, snapshot the framerate and reset. + if (this.lastFpsResetTime > 1000) { + this.lastFps.set(this.currentFps); + + this.currentFps = 0; + this.lastFpsResetTime = 0; + } + } + + + /** + * Timer used to update the framerate. + * This is used because we care about frames in the javaFX thread. + */ + private AnimationTimer timer = new AnimationTimer() { + + long previousFrameTime = System.currentTimeMillis(); + + @Override + public void handle(long now) { + + long currentFrameTime = System.currentTimeMillis(); + long framePeriod = currentFrameTime - previousFrameTime; + + //Increment fps. + incrementFps(framePeriod); + + previousFrameTime = currentFrameTime; + } + }; + + + /** + * Stops the {@link FrameRateTracker}'s timer. + */ + public void stop() { + timer.stop(); + } + + +} diff --git a/racevisionGame/src/main/java/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java index f9fc984e..75e2cf2c 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -6,17 +6,17 @@ import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import network.Messages.Enums.RaceStatusEnum; import network.Messages.Enums.RaceTypeEnum; -import network.Messages.LatestMessages; import shared.dataInput.BoatDataSource; import shared.dataInput.RaceDataSource; import shared.dataInput.RegattaDataSource; +import visualiser.model.VisualiserRaceEvent; import java.util.List; /** * Represents a yacht race. - * This is a base class inherited by {@link mock.model.MockRace} and {@link visualiser.model.VisualiserRace}. + * This is a base class inherited by {@link mock.model.MockRace} and {@link VisualiserRaceEvent}. * Has a course, state, wind, boundaries, etc.... Boats are added by inheriting classes (see {@link Boat}, {@link mock.model.MockBoat}, {@link visualiser.model.VisualiserBoat}. */ public abstract class Race { @@ -37,11 +37,6 @@ public abstract class Race { */ protected RegattaDataSource regattaDataSource; - /** - * The collection of latest race messages. - * Can be either read from or written to. - */ - protected LatestMessages latestMessages; /** * A list of compound marks in the race. @@ -116,16 +111,14 @@ public abstract class Race { * @param boatDataSource Data source for boat related data (yachts and marker boats). * @param raceDataSource Data source for race related data (participating boats, legs, etc...). * @param regattaDataSource Data source for race related data (course name, location, timezone, etc...). - * @param latestMessages The collection of latest messages, which can be written to, or read from. */ - public Race(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages) { + public Race(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource) { //Keep a reference to data sources. this.raceDataSource = raceDataSource; this.boatDataSource = boatDataSource; this.regattaDataSource = regattaDataSource; - this.latestMessages = latestMessages; //Marks. @@ -241,7 +234,7 @@ public abstract class Race { * @param windBearing New wind bearing. * @param windSpeedKnots New wind speed, in knots. */ - protected void setWind(Bearing windBearing, double windSpeedKnots) { + public void setWind(Bearing windBearing, double windSpeedKnots) { Wind wind = new Wind(windBearing, windSpeedKnots); setWind(wind); } @@ -250,7 +243,7 @@ public abstract class Race { * Updates the race to have a specified wind (bearing and speed). * @param wind New wind. */ - protected void setWind(Wind wind) { + public void setWind(Wind wind) { this.raceWind.setValue(wind); } @@ -316,6 +309,23 @@ public abstract class Race { return boundary; } + + /** + * Returns the marks of the race. + * @return Marks of the race. + */ + public List getCompoundMarks() { + return compoundMarks; + } + + /** + * Returns the legs of the race. + * @return Legs of the race. + */ + public List getLegs() { + return legs; + } + /** * Returns the number of frames generated per second. * @return Frames per second. diff --git a/racevisionGame/src/main/java/shared/model/RaceState.java b/racevisionGame/src/main/java/shared/model/RaceState.java new file mode 100644 index 00000000..d77a4129 --- /dev/null +++ b/racevisionGame/src/main/java/shared/model/RaceState.java @@ -0,0 +1,346 @@ +package shared.model; + +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; +import network.Messages.Enums.RaceStatusEnum; +import network.Messages.Enums.RaceTypeEnum; +import shared.dataInput.BoatDataSource; +import shared.dataInput.RaceDataSource; +import shared.dataInput.RegattaDataSource; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + + +/** + * Represents a yacht race. + * This is a base class inherited by {@link mock.model.MockRace} and {@link visualiser.model.VisualiserRaceState}. + * Has a course, state, wind, boundaries, etc.... Boats are added by inheriting classes (see {@link Boat}, {@link mock.model.MockBoat}, {@link visualiser.model.VisualiserBoat}. + */ +public abstract class RaceState { + + + + /** + * Data source for race information. + */ + private RaceDataSource raceDataSource; + + /** + * Data source for boat information. + */ + private BoatDataSource boatDataSource; + + /** + * Data source for regatta information. + */ + private RegattaDataSource regattaDataSource; + + + + /** + * The clock which tracks the race's start time, current time, and elapsed duration. + */ + private RaceClock raceClock; + + + /** + * The current status of the race. + */ + private RaceStatusEnum raceStatusEnum; + + + /** + * The race's wind. + */ + private Property raceWind = new SimpleObjectProperty<>(); + + + + + /** + * Constructs an empty race object. + * This is initialised into a "default" state, with no data. + */ + public RaceState() { + + //Race clock. + this.raceClock = new RaceClock(ZonedDateTime.now()); + + //Race status. + this.setRaceStatusEnum(RaceStatusEnum.NOT_ACTIVE); + + //Wind. + this.setWind(Bearing.fromDegrees(0), 0); + + } + + + + /** + * Initialise the boats in the race. + * This sets their starting positions and current legs. + */ + protected abstract void initialiseBoats(); + + + /** + * Updates the race to use a new list of legs, and adds a dummy "Finish" leg at the end. + * @param legs The new list of legs to use. + */ + protected void useLegsList(List legs) { + //We add a "dummy" leg at the end of the race. + if (legs.size() > 0) { + getLegs().add(new Leg("Finish", getLegs().size())); + } + } + + + /** + * Determines whether or not a specific leg is the last leg in the race. + * @param leg The leg to check. + * @return Returns true if it is the last, false otherwise. + */ + protected boolean isLastLeg(Leg leg) { + + //Get the last leg. + Leg lastLeg = getLegs().get(getLegs().size() - 1); + + //Check its ID. + int lastLegID = lastLeg.getLegNumber(); + + //Get the specified leg's ID. + int legID = leg.getLegNumber(); + + + //Check if they are the same. + return legID == lastLegID; + } + + + /** + * Sets the race data source for the race. + * @param raceDataSource New race data source. + */ + public void setRaceDataSource(RaceDataSource raceDataSource) { + this.raceDataSource = raceDataSource; + this.getRaceClock().setStartingTime(raceDataSource.getStartDateTime()); + } + + /** + * Sets the boat data source for the race. + * @param boatDataSource New boat data source. + */ + public void setBoatDataSource(BoatDataSource boatDataSource) { + this.boatDataSource = boatDataSource; + } + + /** + * Sets the regatta data source for the race. + * @param regattaDataSource New regatta data source. + */ + public void setRegattaDataSource(RegattaDataSource regattaDataSource) { + this.regattaDataSource = regattaDataSource; + } + + + /** + * Returns the race data source for the race. + * @return Race data source. + */ + public RaceDataSource getRaceDataSource() { + return raceDataSource; + } + + /** + * Returns the race data source for the race. + * @return Race data source. + */ + public BoatDataSource getBoatDataSource() { + return boatDataSource; + } + + /** + * Returns the race data source for the race. + * @return Race data source. + */ + public RegattaDataSource getRegattaDataSource() { + return regattaDataSource; + } + + + /** + * Returns a list of {@link Mark} boats. + * @return List of mark boats. + */ + public List getMarks() { + return new ArrayList<>(boatDataSource.getMarkerBoats().values()); + } + + /** + * Returns a list of sourceIDs participating in the race. + * @return List of sourceIDs participating in the race. + */ + public List getParticipants() { + return raceDataSource.getParticipants(); + } + + + + /** + * Returns the current race status. + * @return The current race status. + */ + public RaceStatusEnum getRaceStatusEnum() { + return raceStatusEnum; + } + + /** + * Sets the current race status. + * @param raceStatusEnum The new status of the race. + */ + public void setRaceStatusEnum(RaceStatusEnum raceStatusEnum) { + this.raceStatusEnum = raceStatusEnum; + } + + + /** + * Returns the type of race this is. + * @return The type of race this is. + */ + public RaceTypeEnum getRaceType() { + return raceDataSource.getRaceType(); + } + + + /** + * Returns the name of the regatta. + * @return The name of the regatta. + */ + public String getRegattaName() { + return regattaDataSource.getRegattaName(); + } + + + /** + * Updates the race to have a specified wind bearing and speed. + * @param windBearing New wind bearing. + * @param windSpeedKnots New wind speed, in knots. + */ + public void setWind(Bearing windBearing, double windSpeedKnots) { + Wind wind = new Wind(windBearing, windSpeedKnots); + setWind(wind); + } + + /** + * Updates the race to have a specified wind (bearing and speed). + * @param wind New wind. + */ + public void setWind(Wind wind) { + this.raceWind.setValue(wind); + } + + + /** + * Returns the wind bearing. + * @return The wind bearing. + */ + public Bearing getWindDirection() { + return raceWind.getValue().getWindDirection(); + } + + /** + * Returns the wind speed. + * Measured in knots. + * @return The wind speed. + */ + public double getWindSpeed() { + return raceWind.getValue().getWindSpeed(); + } + + /** + * Returns the race's wind. + * @return The race's wind. + */ + public Property windProperty() { + return raceWind; + } + + /** + * Returns the RaceClock for this race. + * This is used to track the start time, current time, and elapsed duration of the race. + * @return The RaceClock for the race. + */ + public RaceClock getRaceClock() { + return raceClock; + } + + + + /** + * Returns the number of legs in the race. + * @return The number of legs in the race. + */ + public int getLegCount() { + //We minus one, as we have added an extra "dummy" leg. + return getLegs().size() - 1; + } + + + /** + * Returns the race boundary. + * @return The race boundary. + */ + public List getBoundary() { + return raceDataSource.getBoundary(); + } + + + /** + * Returns the marks of the race. + * @return Marks of the race. + */ + public List getCompoundMarks() { + return raceDataSource.getCompoundMarks(); + } + + /** + * Returns the legs of the race. + * @return Legs of the race. + */ + public List getLegs() { + return raceDataSource.getLegs(); + } + + + /** + * Returns the ID of the race. + * @return ID of the race. + */ + public int getRaceId() { + return raceDataSource.getRaceId(); + } + + + /** + * Returns the ID of the regatta. + * @return The ID of the regatta. + */ + public int getRegattaID() { + return regattaDataSource.getRegattaID(); + } + + + /** + * Returns the name of the course. + * @return Name of the course. + */ + public String getCourseName() { + return regattaDataSource.getCourseName(); + } + + + + +} diff --git a/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java index 7ab12532..fd5827f0 100644 --- a/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java +++ b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java @@ -50,4 +50,5 @@ public interface RunnableWithFramePeriod extends Runnable { } + } diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java index bac06db3..27c87a08 100644 --- a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java @@ -30,8 +30,12 @@ public class ConnectionToServerCommandFactory { switch(joinAcceptance.getAcceptanceType()) { - case JOIN_SUCCESSFUL: return new JoinSuccessfulCommand(joinAcceptance, connectionToServer); - case RACE_PARTICIPANTS_FULL: return new RaceParticipantsFullCommand(joinAcceptance, connectionToServer); + case JOIN_SUCCESSFUL_PARTICIPANT: return new JoinSuccessParticipantCommand(joinAcceptance, connectionToServer); + + case JOIN_SUCCESSFUL_SPECTATOR: return new JoinSuccessSpectatorCommand(joinAcceptance, connectionToServer); + + case JOIN_FAILURE: return new JoinFailureCommand(joinAcceptance, connectionToServer); + case SERVER_FULL: return new ServerFullCommand(joinAcceptance, connectionToServer); default: throw new CommandConstructionException("Could not create command for JoinAcceptance: " + joinAcceptance + ". Unknown JoinAcceptanceEnum."); diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java similarity index 76% rename from racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java rename to racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java index ae631c6e..283d6fc2 100644 --- a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java @@ -5,13 +5,11 @@ import network.Messages.JoinAcceptance; import visualiser.enums.ConnectionToServerState; import visualiser.network.ConnectionToServer; -import java.util.Optional; - /** * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#RACE_PARTICIPANTS_FULL} {@link JoinAcceptance} message is received. */ -public class RaceParticipantsFullCommand implements Command { +public class JoinFailureCommand implements Command { /** * The message to operate on. @@ -25,11 +23,11 @@ public class RaceParticipantsFullCommand implements Command { /** - * Creates a new {@link RaceParticipantsFullCommand}, which operates on a given {@link ConnectionToServer}. + * Creates a new {@link JoinFailureCommand}, which operates on a given {@link ConnectionToServer}. * @param joinAcceptance The message to operate on. * @param connectionToServer The context to operate on. */ - public RaceParticipantsFullCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { + public JoinFailureCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { this.joinAcceptance = joinAcceptance; this.connectionToServer = connectionToServer; } diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessParticipantCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessParticipantCommand.java new file mode 100644 index 00000000..da29664c --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessParticipantCommand.java @@ -0,0 +1,57 @@ +package visualiser.Commands.ConnectionToServerCommands; + +import mock.model.commandFactory.Command; +import network.Messages.AssignPlayerBoat; +import network.Messages.JoinAcceptance; +import visualiser.enums.ConnectionToServerState; +import visualiser.network.ConnectionToServer; + +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL_PARTICIPANT} {@link network.Messages.JoinAcceptance} message is received. + */ +public class JoinSuccessParticipantCommand implements Command { + + /** + * The message to operate on. + */ + private JoinAcceptance joinAcceptance; + + /** + * The context to operate on. + */ + private ConnectionToServer connectionToServer; + + + /** + * Creates a new {@link JoinSuccessParticipantCommand}, which operates on a given {@link ConnectionToServer}. + * @param joinAcceptance The message to operate on. + * @param connectionToServer The context to operate on. + */ + public JoinSuccessParticipantCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { + this.joinAcceptance = joinAcceptance; + this.connectionToServer = connectionToServer; + } + + + + @Override + public void execute() { + + connectionToServer.setJoinAcceptance(joinAcceptance); + + connectionToServer.setConnectionState(ConnectionToServerState.CONNECTED); + + + AssignPlayerBoat assignPlayerBoat = new AssignPlayerBoat(joinAcceptance.getSourceID()); + try { + connectionToServer.send(assignPlayerBoat); + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.WARNING, "JoinSuccessParticipantCommand: " + this + " was interrupted on thread: " + Thread.currentThread() + " while sending AssignPlayerBoat message.", e); + } + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessSpectatorCommand.java similarity index 71% rename from racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java rename to racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessSpectatorCommand.java index 8cdfea5b..6e7032fa 100644 --- a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessSpectatorCommand.java @@ -5,13 +5,11 @@ import network.Messages.JoinAcceptance; import visualiser.enums.ConnectionToServerState; import visualiser.network.ConnectionToServer; -import java.util.Optional; - /** - * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL} {@link network.Messages.JoinAcceptance} message is received. + * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL_PARTICIPANT} {@link JoinAcceptance} message is received. */ -public class JoinSuccessfulCommand implements Command { +public class JoinSuccessSpectatorCommand implements Command { /** * The message to operate on. @@ -25,11 +23,11 @@ public class JoinSuccessfulCommand implements Command { /** - * Creates a new {@link JoinSuccessfulCommand}, which operates on a given {@link ConnectionToServer}. + * Creates a new {@link JoinSuccessSpectatorCommand}, which operates on a given {@link ConnectionToServer}. * @param joinAcceptance The message to operate on. * @param connectionToServer The context to operate on. */ - public JoinSuccessfulCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { + public JoinSuccessSpectatorCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { this.joinAcceptance = joinAcceptance; this.connectionToServer = connectionToServer; } diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/AssignPlayerBoatCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/AssignPlayerBoatCommand.java new file mode 100644 index 00000000..fd265325 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/AssignPlayerBoatCommand.java @@ -0,0 +1,53 @@ +package visualiser.Commands.VisualiserRaceCommands; + +import mock.model.commandFactory.Command; +import network.Messages.AssignPlayerBoat; +import network.Messages.BoatLocation; +import shared.exceptions.BoatNotFoundException; +import shared.exceptions.MarkNotFoundException; +import shared.model.GPSCoordinate; +import shared.model.Mark; +import visualiser.model.VisualiserBoat; +import visualiser.model.VisualiserRaceState; + +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Command created when a {@link AssignPlayerBoat} message is received. + */ +public class AssignPlayerBoatCommand implements Command { + + /** + * The message to operate on. + */ + private AssignPlayerBoat assignPlayerBoat; + + /** + * The context to operate on. + */ + private VisualiserRaceState visualiserRace; + + + /** + * Creates a new {@link AssignPlayerBoatCommand}, which operates on a given {@link VisualiserRaceState}. + * @param assignPlayerBoat The message to operate on. + * @param visualiserRace The context to operate on. + */ + public AssignPlayerBoatCommand(AssignPlayerBoat assignPlayerBoat, VisualiserRaceState visualiserRace) { + this.assignPlayerBoat = assignPlayerBoat; + this.visualiserRace = visualiserRace; + } + + + + @Override + public void execute() { + + visualiserRace.setPlayerBoatID(assignPlayerBoat.getSourceID()); + + } + + +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatLocationCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatLocationCommand.java new file mode 100644 index 00000000..d2c60bd7 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatLocationCommand.java @@ -0,0 +1,127 @@ +package visualiser.Commands.VisualiserRaceCommands; + +import mock.model.commandFactory.Command; +import network.Messages.BoatLocation; +import network.Messages.Enums.BoatStatusEnum; +import shared.exceptions.BoatNotFoundException; +import shared.exceptions.MarkNotFoundException; +import shared.model.GPSCoordinate; +import shared.model.Mark; +import visualiser.model.VisualiserBoat; +import visualiser.model.VisualiserRaceEvent; +import visualiser.model.VisualiserRaceState; + +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Command created when a {@link BoatLocation} message is received. + */ +public class BoatLocationCommand implements Command { + + /** + * The message to operate on. + */ + private BoatLocation boatLocation; + + /** + * The context to operate on. + */ + private VisualiserRaceState visualiserRace; + + + /** + * Creates a new {@link BoatLocationCommand}, which operates on a given {@link VisualiserRaceState}. + * @param boatLocation The message to operate on. + * @param visualiserRace The context to operate on. + */ + public BoatLocationCommand(BoatLocation boatLocation, VisualiserRaceState visualiserRace) { + this.boatLocation = boatLocation; + this.visualiserRace = visualiserRace; + } + + + + @Override + public void execute() { + + if (visualiserRace.isVisualiserBoat(boatLocation.getSourceID())) { + updateBoatLocation(); + } else if (visualiserRace.isMark(boatLocation.getSourceID())) { + updateMarkLocation(); + } + + } + + + /** + * Updates the boat specified in the message. + */ + private void updateBoatLocation() { + + try { + VisualiserBoat boat = visualiserRace.getBoat(boatLocation.getSourceID()); + + //Get the new position. + GPSCoordinate gpsCoordinate = new GPSCoordinate( + boatLocation.getLatitude(), + boatLocation.getLongitude()); + + boat.setCurrentPosition(gpsCoordinate); + + //Bearing. + boat.setBearing(boatLocation.getHeading()); + + + //Speed. + boat.setCurrentSpeed(boatLocation.getBoatSpeedKnots()); + + + //Attempt to add a track point. + attemptAddTrackPoint(boat); + + + } catch (BoatNotFoundException e) { + Logger.getGlobal().log(Level.WARNING, "BoatLocationCommand: " + this + " could not execute. Boat with sourceID: " + boatLocation.getSourceID() + " not found.", e); + return; + } + + } + + + /** + * Attempts to add a track point to the boat. Only works if the boat is currently racing. + * @param boat The boat to add a track point to. + */ + private void attemptAddTrackPoint(VisualiserBoat boat) { + if (boat.getStatus() == BoatStatusEnum.RACING) { + boat.addTrackPoint(boat.getCurrentPosition(), visualiserRace.getRaceClock().getCurrentTime()); + } + } + + + /** + * Updates the marker boat specified in message. + */ + private void updateMarkLocation() { + + try { + Mark mark = visualiserRace.getMark(boatLocation.getSourceID()); + + GPSCoordinate gpsCoordinate = new GPSCoordinate( + boatLocation.getLatitude(), + boatLocation.getLongitude()); + + mark.setPosition(gpsCoordinate); + } catch (MarkNotFoundException e) { + Logger.getGlobal().log(Level.WARNING, "BoatLocationCommand: " + this + " could not execute. Mark with sourceID: " + boatLocation.getSourceID() + " not found.", e); + return; + + } + + + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatsXMLMessageCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatsXMLMessageCommand.java new file mode 100644 index 00000000..7f56d745 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatsXMLMessageCommand.java @@ -0,0 +1,48 @@ +package visualiser.Commands.VisualiserRaceCommands; + +import mock.model.commandFactory.Command; +import network.Messages.XMLMessage; +import shared.dataInput.BoatDataSource; +import shared.dataInput.BoatXMLReader; +import shared.enums.XMLFileType; +import shared.exceptions.InvalidBoatDataException; +import shared.exceptions.XMLReaderException; +import visualiser.model.VisualiserRaceEvent; +import visualiser.model.VisualiserRaceState; + + +/** + * Command created when a {@link network.Messages.Enums.XMLMessageType#BOAT} {@link XMLMessage} message is received. + */ +public class BoatsXMLMessageCommand implements Command { + + /** + * The data source to operate on. + */ + private BoatDataSource boatDataSource; + + /** + * The context to operate on. + */ + private VisualiserRaceState visualiserRace; + + + /** + * Creates a new {@link BoatsXMLMessageCommand}, which operates on a given {@link VisualiserRaceEvent}. + * @param boatDataSource The data source to operate on. + * @param visualiserRace The context to operate on. + */ + public BoatsXMLMessageCommand(BoatDataSource boatDataSource, VisualiserRaceState visualiserRace) { + this.boatDataSource = boatDataSource; + this.visualiserRace = visualiserRace; + } + + + + @Override + public void execute() { + + visualiserRace.setBoatDataSource(boatDataSource); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java new file mode 100644 index 00000000..11fd8c3b --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java @@ -0,0 +1,185 @@ +package visualiser.Commands.VisualiserRaceCommands; + +import mock.model.commandFactory.Command; +import network.Messages.BoatStatus; +import network.Messages.Enums.BoatStatusEnum; +import network.Messages.RaceStatus; +import shared.exceptions.BoatNotFoundException; +import shared.model.Leg; +import visualiser.model.VisualiserBoat; +import visualiser.model.VisualiserRaceEvent; +import visualiser.model.VisualiserRaceState; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Command created when a {@link RaceStatus} message is received. + */ +public class RaceStatusCommand implements Command { + + /** + * The message to operate on. + */ + private RaceStatus raceStatus; + + /** + * The context to operate on. + */ + private VisualiserRaceState visualiserRace; + + + /** + * Creates a new {@link RaceStatusCommand}, which operates on a given {@link VisualiserRaceState}. + * @param raceStatus The message to operate on. + * @param visualiserRace The context to operate on. + */ + public RaceStatusCommand(RaceStatus raceStatus, VisualiserRaceState visualiserRace) { + this.raceStatus = raceStatus; + this.visualiserRace = visualiserRace; + } + + + + @Override + public void execute() { + + //Race status enum. + visualiserRace.setRaceStatusEnum(raceStatus.getRaceStatus()); + + //Wind. + visualiserRace.setWind( + raceStatus.getWindDirection(), + raceStatus.getWindSpeed() ); + + //Current race time. + visualiserRace.getRaceClock().setUTCTime(raceStatus.getCurrentTime()); + + + + for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) { + updateBoatStatus(boatStatus); + } + + + visualiserRace.updateBoatPositions(visualiserRace.getBoats()); + + + + } + + + /** + * Updates a single boat's status using the boatStatus message. + * @param boatStatus BoatStatus message to get data from. + */ + private void updateBoatStatus(BoatStatus boatStatus) { + try { + VisualiserBoat boat = visualiserRace.getBoat(boatStatus.getSourceID()); + + //Time at next mark. + updateEstimatedTimeAtNextMark(boatStatus, boat); + + + BoatStatusEnum newBoatStatusEnum = boatStatus.getBoatStatus(); + + //Time at last mark. + initialiseTimeAtLastMark(boat, boat.getStatus(), newBoatStatusEnum); + + //Status. + boat.setStatus(newBoatStatusEnum); + + + List legs = visualiserRace.getLegs(); + + //Leg. + updateLeg(boatStatus.getLegNumber(), boat, legs); + + + //Set finish time if boat finished. + attemptUpdateFinishTime(boatStatus, boat, legs); + + + } catch (BoatNotFoundException e) { + Logger.getGlobal().log(Level.WARNING, "RaceStatusCommand.updateBoatStatus: " + this + " could not execute. Boat with sourceID: " + boatStatus.getSourceID() + " not found.", e); + return; + } + } + + + /** + * Attempts to update the finish time of the boat. Only works if the boat has actually finished the race. + * @param boatStatus BoatStatus to read data from. + * @param boat Boat to update. + * @param legs Legs of the race. + */ + private void attemptUpdateFinishTime(BoatStatus boatStatus, VisualiserBoat boat, List legs) { + + if (boat.getStatus() == BoatStatusEnum.FINISHED || boatStatus.getLegNumber() == legs.size()) { + boat.setTimeFinished(visualiserRace.getRaceClock().getCurrentTimeMilli()); + boat.setStatus(BoatStatusEnum.FINISHED); + } + + } + + + + /** + * Updates a boat's leg. + * @param legNumber The new leg number. + * @param boat The boat to update. + * @param legs The legs in the race. + */ + private void updateLeg(int legNumber, VisualiserBoat boat, List legs) { + + if (legNumber >= 1 && legNumber < legs.size()) { + if (boat.getCurrentLeg() != legs.get(legNumber)) { + boatFinishedLeg(boat, legs.get(legNumber)); + } + } + } + + /** + * Initialises the time at last mark for a boat. Only changes if the boat's status is changing from non-racing to racing. + * @param boat The boat to update. + * @param currentBoatStatus The current status of the boat. + * @param newBoatStatusEnum The new status of the boat, from the BoatStatus message. + */ + private void initialiseTimeAtLastMark(VisualiserBoat boat, BoatStatusEnum currentBoatStatus, BoatStatusEnum newBoatStatusEnum) { + //If we are changing from non-racing to racing, we need to initialise boat with their time at last mark. + if ((currentBoatStatus != BoatStatusEnum.RACING) && (newBoatStatusEnum == BoatStatusEnum.RACING)) { + boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime()); + } + } + + + /** + * Updates the estimated time at next mark for a given boat. + * @param boatStatus BoatStatus to read data from. + * @param boat Boat to update. + */ + private void updateEstimatedTimeAtNextMark(BoatStatus boatStatus, VisualiserBoat boat) { + boat.setEstimatedTimeAtNextMark(visualiserRace.getRaceClock().getLocalTime(boatStatus.getEstTimeAtNextMark())); + } + + + + /** + * Updates a boat's leg to a specified leg. Also records the order in which the boat passed the leg. + * @param boat The boat to update. + * @param leg The leg to use. + */ + private void boatFinishedLeg(VisualiserBoat boat, Leg leg) { + + //Record order in which boat finished leg. + visualiserRace.getLegCompletionOrder().get(boat.getCurrentLeg()).add(boat); + + //Update boat. + boat.setCurrentLeg(leg); + boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime()); + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceXMLMessageCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceXMLMessageCommand.java new file mode 100644 index 00000000..0b194674 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceXMLMessageCommand.java @@ -0,0 +1,44 @@ +package visualiser.Commands.VisualiserRaceCommands; + +import mock.model.commandFactory.Command; +import network.Messages.XMLMessage; +import shared.dataInput.RaceDataSource; +import visualiser.model.VisualiserRaceEvent; +import visualiser.model.VisualiserRaceState; + + +/** + * Command created when a {@link network.Messages.Enums.XMLMessageType#BOAT} {@link XMLMessage} message is received. + */ +public class RaceXMLMessageCommand implements Command { + + /** + * The data source to operate on. + */ + private RaceDataSource raceDataSource; + + /** + * The context to operate on. + */ + private VisualiserRaceState visualiserRace; + + + /** + * Creates a new {@link RaceXMLMessageCommand}, which operates on a given {@link VisualiserRaceEvent}. + * @param raceDataSource The data source to operate on. + * @param visualiserRace The context to operate on. + */ + public RaceXMLMessageCommand(RaceDataSource raceDataSource, VisualiserRaceState visualiserRace) { + this.raceDataSource = raceDataSource; + this.visualiserRace = visualiserRace; + } + + + + @Override + public void execute() { + + visualiserRace.setRaceDataSource(raceDataSource); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RegattaXMLMessageCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RegattaXMLMessageCommand.java new file mode 100644 index 00000000..6597e557 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RegattaXMLMessageCommand.java @@ -0,0 +1,44 @@ +package visualiser.Commands.VisualiserRaceCommands; + +import mock.model.commandFactory.Command; +import network.Messages.XMLMessage; +import shared.dataInput.RegattaDataSource; +import visualiser.model.VisualiserRaceEvent; +import visualiser.model.VisualiserRaceState; + + +/** + * Command created when a {@link network.Messages.Enums.XMLMessageType#BOAT} {@link XMLMessage} message is received. + */ +public class RegattaXMLMessageCommand implements Command { + + /** + * The data source to operate on. + */ + private RegattaDataSource regattaDataSource; + + /** + * The context to operate on. + */ + private VisualiserRaceState visualiserRace; + + + /** + * Creates a new {@link RegattaXMLMessageCommand}, which operates on a given {@link VisualiserRaceEvent}. + * @param regattaDataSource The data source to operate on. + * @param visualiserRace The context to operate on. + */ + public RegattaXMLMessageCommand(RegattaDataSource regattaDataSource, VisualiserRaceState visualiserRace) { + this.regattaDataSource = regattaDataSource; + this.visualiserRace = visualiserRace; + } + + + + @Override + public void execute() { + + visualiserRace.setRegattaDataSource(regattaDataSource); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java new file mode 100644 index 00000000..37755e91 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java @@ -0,0 +1,40 @@ +package visualiser.Commands.VisualiserRaceCommands; + + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.*; +import visualiser.model.VisualiserRaceEvent; +import visualiser.model.VisualiserRaceState; + +/** + * Factory to create VisualiserRace commands. + */ +public class VisualiserRaceCommandFactory { + + /** + * Generates a command on an VisualiserRace. + * @param message The message to turn into a command. + * @param visualiserRace The context for the command to operate on. + * @return The command to execute the given action. + * @throws CommandConstructionException Thrown if the command cannot be constructed. + */ + public static Command create(AC35Data message, VisualiserRaceState visualiserRace) throws CommandConstructionException { + + switch (message.getType()) { + + case BOATLOCATION: return new BoatLocationCommand((BoatLocation) message, visualiserRace); + + case RACESTATUS: return new RaceStatusCommand((RaceStatus) message, visualiserRace); + + case XMLMESSAGE: return XMLMessageCommandFactory.create((XMLMessage) message, visualiserRace); + + case ASSIGN_PLAYER_BOAT: return new AssignPlayerBoatCommand((AssignPlayerBoat) message, visualiserRace); + + default: throw new CommandConstructionException("Could not create VisualiserRaceCommand. Unrecognised or unsupported MessageType: " + message.getType()); + + } + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/XMLMessageCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/XMLMessageCommandFactory.java new file mode 100644 index 00000000..b9b58f1c --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/XMLMessageCommandFactory.java @@ -0,0 +1,63 @@ +package visualiser.Commands.VisualiserRaceCommands; + + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.BoatLocation; +import network.Messages.RaceStatus; +import network.Messages.XMLMessage; +import shared.dataInput.*; +import shared.enums.XMLFileType; +import shared.exceptions.InvalidBoatDataException; +import shared.exceptions.InvalidRaceDataException; +import shared.exceptions.InvalidRegattaDataException; +import shared.exceptions.XMLReaderException; +import visualiser.model.VisualiserRaceState; + +/** + * Factory to create VisualiserRace commands, from XMLMessages. + */ +public class XMLMessageCommandFactory { + + /** + * Generates a command on an VisualiserRace. + * @param message The message to turn into a command. + * @param visualiserRace The context for the command to operate on. + * @return The command to execute the given action. + * @throws CommandConstructionException Thrown if the command cannot be constructed. + */ + public static Command create(XMLMessage message, VisualiserRaceState visualiserRace) throws CommandConstructionException { + + try { + + switch (message.getXmlMsgSubType()) { + + case BOAT: + BoatDataSource boatDataSource = new BoatXMLReader(message.getXmlMessage(), XMLFileType.Contents); + return new BoatsXMLMessageCommand(boatDataSource, visualiserRace); + + + case RACE: + RaceDataSource raceDataSource = new RaceXMLReader(message.getXmlMessage(), XMLFileType.Contents); + return new RaceXMLMessageCommand(raceDataSource, visualiserRace); + + + case REGATTA: + RegattaDataSource regattaDataSource = new RegattaXMLReader(message.getXmlMessage(), XMLFileType.Contents); + return new RegattaXMLMessageCommand(regattaDataSource, visualiserRace); + + + default: + throw new CommandConstructionException("Could not create VisualiserRaceCommand/XMLCommand. Unrecognised or unsupported MessageType: " + message.getType()); + + } + + } catch (XMLReaderException | InvalidBoatDataException | InvalidRegattaDataException | InvalidRaceDataException e) { + throw new CommandConstructionException("Could not create VisualiserRaceCommand/XMLCommand. Could not parse XML message payload.", e); + + } + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java b/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java index 3e81cd16..ab9bd421 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java @@ -4,7 +4,6 @@ package visualiser.Controllers; import javafx.application.Platform; import javafx.beans.property.Property; import javafx.fxml.FXML; -import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.image.ImageView; import javafx.scene.layout.Pane; @@ -12,7 +11,6 @@ import javafx.scene.layout.StackPane; import javafx.scene.shape.Circle; import shared.model.Bearing; import shared.model.Wind; -import visualiser.model.VisualiserRace; /** * Controller for the arrow.fxml view. diff --git a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java index c801f16a..7cdd0e73 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java @@ -4,14 +4,14 @@ import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.layout.AnchorPane; import visualiser.gameController.ControllerClient; -import visualiser.network.ServerConnection; import visualiser.model.VisualiserBoat; -import visualiser.model.VisualiserRace; +import visualiser.model.VisualiserRaceEvent; import java.net.Socket; import java.net.URL; import java.util.ResourceBundle; + /** * Controller that everything is overlayed onto. This makes it so that changing scenes does not resize your stage. */ @@ -38,10 +38,9 @@ public class MainController extends Controller { * Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race). * @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(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) { - raceController.startRace(visualiserRace, controllerClient, serverConnection); + public void beginRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient) { + raceController.startRace(visualiserRace, controllerClient); } /** diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index 2e1a811e..be5b5cd1 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -4,7 +4,9 @@ package visualiser.Controllers; import javafx.animation.AnimationTimer; import javafx.application.Platform; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.scene.chart.LineChart; import javafx.scene.control.*; @@ -28,6 +30,8 @@ import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; + + /** * Controller used to display a running race. */ @@ -37,12 +41,16 @@ public class RaceController extends Controller { /** * The race object which describes the currently occurring race. */ - private VisualiserRace visualiserRace; + private VisualiserRaceEvent visualiserRace; + /** - * An additional observable list of boats. This is used by the table view, to allow it to sort boats without effecting the race's own list of boats. + * Service for sending keystrokes to server */ - private ObservableList tableBoatList; + private ControllerClient controllerClient; + + + /** * The canvas that draws the race. @@ -59,15 +67,6 @@ public class RaceController extends Controller { */ @FXML private ArrowController arrowController; - /** - * Service for sending keystrokes to server - */ - private ControllerClient controllerClient; - - /** - * The connection to the server. - */ - private ServerConnection serverConnection; @FXML private GridPane canvasBase; @@ -162,7 +161,7 @@ public class RaceController extends Controller { * Initialises the frame rate functionality. This allows for toggling the frame rate, and connect the fps label to the race's fps property. * @param visualiserRace The race to connect the fps label to. */ - private void initialiseFps(VisualiserRace visualiserRace) { + private void initialiseFps(VisualiserRaceEvent visualiserRace) { //On/off toggle. initialiseFpsToggle(); @@ -192,9 +191,9 @@ public class RaceController extends Controller { * Initialises the fps label to update when the race fps changes. * @param visualiserRace The race to monitor the frame rate of. */ - private void initialiseFpsLabel(VisualiserRace visualiserRace) { + private void initialiseFpsLabel(VisualiserRaceEvent visualiserRace) { - visualiserRace.fpsProperty().addListener((observable, oldValue, newValue) -> { + visualiserRace.getFrameRateProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> this.FPS.setText("FPS: " + newValue.toString())); }); @@ -206,14 +205,15 @@ public class RaceController extends Controller { * Initialises the information table view to listen to a given race. * @param race Race to listen to. */ - public void initialiseInfoTable(VisualiserRace race) { + public void initialiseInfoTable(VisualiserRaceEvent race) { //Copy list of boats. - this.tableBoatList = FXCollections.observableArrayList(race.getBoats()); + SortedList sortedBoats = new SortedList<>(race.getVisualiserRaceState().getBoats()); + sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty()); //Set up table. - boatInfoTable.setItems(this.tableBoatList); + boatInfoTable.setItems(sortedBoats); //Set up each column. @@ -293,12 +293,12 @@ public class RaceController extends Controller { /** - * Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRace}. + * Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRaceEvent}. * @param race The race to listen to. */ - private void initialiseSparkline(VisualiserRace race) { + private void initialiseSparkline(VisualiserRaceEvent race) { //The race.getBoats() we are passing in is sorted by position in race inside the race class. - this.sparkline = new Sparkline(this.visualiserRace, this.sparklineChart); + this.sparkline = new Sparkline(this.visualiserRace.getVisualiserRaceState(), this.sparklineChart); } @@ -306,7 +306,7 @@ public class RaceController extends Controller { * Initialises the {@link ResizableRaceCanvas}, provides the race to read data from. * @param race Race to read data from. */ - private void initialiseRaceCanvas(VisualiserRace race) { + private void initialiseRaceCanvas(VisualiserRaceEvent race) { //Create canvas. raceCanvas = new ResizableRaceCanvas(race); @@ -329,18 +329,18 @@ public class RaceController extends Controller { * Intialises the race time zone label with the race's time zone. * @param race The race to get time zone from. */ - private void initialiseRaceTimezoneLabel(VisualiserRace race) { - timeZone.setText(race.getRaceClock().getTimeZone()); + private void initialiseRaceTimezoneLabel(VisualiserRaceEvent race) { + timeZone.setText(race.getVisualiserRaceState().getRaceClock().getTimeZone()); } /** * Initialises the race clock to listen to the specified race. * @param race The race to listen to. */ - private void initialiseRaceClock(VisualiserRace race) { + private void initialiseRaceClock(VisualiserRaceEvent race) { //RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update. - race.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> { + race.getVisualiserRaceState().getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { timer.setText(newValue); }); @@ -353,13 +353,11 @@ public class RaceController extends Controller { * Displays a specified race. * @param visualiserRace Object modelling the race. * @param controllerClient Socket Client that manipulates the controller. - * @param serverConnection The connection to the server. */ - public void startRace(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) { + public void startRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient) { this.visualiserRace = visualiserRace; this.controllerClient = controllerClient; - this.serverConnection = serverConnection; initialiseRace(); @@ -375,7 +373,7 @@ public class RaceController extends Controller { * Transition from the race view to the finish view. * @param boats boats there are in the race. */ - public void finishRace(ObservableList boats){ + public void finishRace(ObservableList boats) { race.setVisible(false); parent.enterFinish(boats); } @@ -385,8 +383,8 @@ public class RaceController extends Controller { * Initialises the arrow controller with data from the race to observe. * @param race The race to observe. */ - private void initialiseArrow(VisualiserRace race) { - arrowController.setWindProperty(race.windProperty()); + private void initialiseArrow(VisualiserRaceEvent race) { + arrowController.setWindProperty(race.getVisualiserRaceState().windProperty()); } @@ -399,7 +397,7 @@ public class RaceController extends Controller { public void handle(long arg0) { //Get the current race status. - RaceStatusEnum raceStatus = visualiserRace.getRaceStatusEnum(); + RaceStatusEnum raceStatus = visualiserRace.getVisualiserRaceState().getRaceStatusEnum(); //If the race has finished, go to finish view. @@ -408,7 +406,7 @@ public class RaceController extends Controller { stop(); //Hide this, and display the finish controller. - finishRace(visualiserRace.getBoats()); + finishRace(visualiserRace.getVisualiserRaceState().getBoats()); } else { //Otherwise, render the canvas. @@ -421,7 +419,7 @@ public class RaceController extends Controller { } //Return to main screen if we lose connection. - if (!serverConnection.isAlive()) { + if (!visualiserRace.getServerConnection().isAlive()) { race.setVisible(false); parent.enterTitle(); //TODO currently this doesn't work correctly - the title screen remains visible after clicking join game diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index b1c17bb1..b2d6b2b7 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -9,7 +9,7 @@ import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; -import javafx.scene.paint.Color; +import mock.model.commandFactory.CompositeCommand; import network.Messages.Enums.RaceStatusEnum; import network.Messages.Enums.RequestToJoinEnum; import network.Messages.LatestMessages; @@ -20,9 +20,10 @@ import shared.exceptions.InvalidRaceDataException; import shared.exceptions.InvalidRegattaDataException; import shared.exceptions.XMLReaderException; import visualiser.gameController.ControllerClient; +import visualiser.model.VisualiserRaceState; import visualiser.network.ServerConnection; import visualiser.model.VisualiserBoat; -import visualiser.model.VisualiserRace; +import visualiser.model.VisualiserRaceEvent; import java.io.IOException; import java.net.Socket; @@ -31,10 +32,11 @@ import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; + /** * Controller to for waiting for the race to start. */ -public class StartController extends Controller implements Observer { +public class StartController extends Controller { @FXML private GridPane start; @FXML private AnchorPane startWrapper; @@ -70,9 +72,10 @@ public class StartController extends Controller implements Observer { /** - * Our connection to the server. + * The race + connection to server. */ - private ServerConnection serverConnection; + private VisualiserRaceEvent visualiserRaceEvent; + /** * Writes BoatActions to outgoing message queue. @@ -80,27 +83,9 @@ public class StartController extends Controller implements Observer { private ControllerClient controllerClient; - /** - * The race object which describes the currently occurring race. - */ - private VisualiserRace visualiserRace; - /** - * An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor. - */ - private List colors = new ArrayList<>(Arrays.asList( - Color.BLUEVIOLET, - Color.BLACK, - Color.RED, - Color.ORANGE, - Color.DARKOLIVEGREEN, - Color.LIMEGREEN, - Color.PURPLE, - Color.DARKGRAY, - Color.YELLOW - )); @@ -117,41 +102,17 @@ public class StartController extends Controller implements Observer { /** * Starts the race. - * Called once we have received all XML files from the server. - * @param latestMessages The set of latest race messages to use for race. - * @throws XMLReaderException Thrown if XML file cannot be parsed. - * @throws InvalidRaceDataException Thrown if XML file cannot be parsed. - * @throws InvalidBoatDataException Thrown if XML file cannot be parsed. - * @throws InvalidRegattaDataException Thrown if XML file cannot be parsed. */ - private void startRace(LatestMessages latestMessages) throws XMLReaderException, InvalidRaceDataException, InvalidBoatDataException, InvalidRegattaDataException { - - //Create data sources from latest messages for the race. - RaceDataSource raceDataSource = new RaceXMLReader(latestMessages.getRaceXMLMessage().getXmlMessage(), XMLFileType.Contents); - BoatDataSource boatDataSource = new BoatXMLReader(latestMessages.getBoatXMLMessage().getXmlMessage(), XMLFileType.Contents); - RegattaDataSource regattaDataSource = new RegattaXMLReader(latestMessages.getRegattaXMLMessage().getXmlMessage(), XMLFileType.Contents); - - //Create race. - this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors); - new Thread(this.visualiserRace).start(); - - //Ugly. TODO refactor - ObservableList boats = visualiserRace.getBoats(); - for (VisualiserBoat boat : boats) { - if (boat.getSourceID() == serverConnection.getAllocatedSourceID()) { - boat.setClientBoat(true); - } - } - + private void startRace() { //Initialise the boat table. - initialiseBoatTable(this.visualiserRace); + initialiseBoatTable(this.visualiserRaceEvent.getVisualiserRaceState()); //Initialise the race name. - initialiseRaceName(this.visualiserRace); + initialiseRaceName(this.visualiserRaceEvent.getVisualiserRaceState()); //Initialises the race clock. - initialiseRaceClock(this.visualiserRace); + initialiseRaceClock(this.visualiserRaceEvent.getVisualiserRaceState()); //Starts the race countdown timer. countdownTimer(); @@ -169,7 +130,7 @@ public class StartController extends Controller implements Observer { * Initialises the boat table that is to be shown on the pane. * @param visualiserRace The race to get data from. */ - private void initialiseBoatTable(VisualiserRace visualiserRace) { + private void initialiseBoatTable(VisualiserRaceState visualiserRace) { //Get the boats. ObservableList boats = visualiserRace.getBoats(); @@ -184,7 +145,7 @@ public class StartController extends Controller implements Observer { * Initialises the race name which is shown on the pane. * @param visualiserRace The race to get data from. */ - private void initialiseRaceName(VisualiserRace visualiserRace) { + private void initialiseRaceName(VisualiserRaceState visualiserRace) { raceTitleLabel.setText(visualiserRace.getRegattaName()); @@ -194,7 +155,7 @@ public class StartController extends Controller implements Observer { * Initialises the race clock/timer labels for the start time, current time, and remaining time. * @param visualiserRace The race to get data from. */ - private void initialiseRaceClock(VisualiserRace visualiserRace) { + private void initialiseRaceClock(VisualiserRaceState visualiserRace) { //Start time. initialiseRaceClockStartTime(visualiserRace); @@ -212,7 +173,7 @@ public class StartController extends Controller implements Observer { * Initialises the race current time label. * @param visualiserRace The race to get data from. */ - private void initialiseRaceClockStartTime(VisualiserRace visualiserRace) { + private void initialiseRaceClockStartTime(VisualiserRaceState visualiserRace) { raceStartLabel.setText(visualiserRace.getRaceClock().getStartingTimeString()); @@ -229,7 +190,7 @@ public class StartController extends Controller implements Observer { * Initialises the race current time label. * @param visualiserRace The race to get data from. */ - private void initialiseRaceClockCurrentTime(VisualiserRace visualiserRace) { + private void initialiseRaceClockCurrentTime(VisualiserRaceState visualiserRace) { visualiserRace.getRaceClock().currentTimeProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { @@ -243,7 +204,7 @@ public class StartController extends Controller implements Observer { * Initialises the race duration label. * @param visualiserRace The race to get data from. */ - private void initialiseRaceClockDuration(VisualiserRace visualiserRace) { + private void initialiseRaceClockDuration(VisualiserRaceState visualiserRace) { visualiserRace.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { @@ -261,13 +222,13 @@ public class StartController extends Controller implements Observer { @Override public void handle(long arg0) { - //TODO instead of having an AnimationTimer checking the race status, we could provide a Property, and connect a listener to that. //Get the current race status. - RaceStatusEnum raceStatus = visualiserRace.getRaceStatusEnum(); + RaceStatusEnum raceStatus = visualiserRaceEvent.getVisualiserRaceState().getRaceStatusEnum(); //Display it. raceStatusLabel.setText("Race Status: " + raceStatus.name()); + //If the race has reached the preparatory phase, or has started... if (raceStatus == RaceStatusEnum.PREPARATORY || raceStatus == RaceStatusEnum.STARTED) { //Stop this timer. @@ -275,9 +236,10 @@ public class StartController extends Controller implements Observer { //Hide this, and display the race controller. startWrapper.setVisible(false); - start.setVisible(false); + //start.setVisible(false);//TODO is this needed? + + parent.beginRace(visualiserRaceEvent, controllerClient); - parent.beginRace(visualiserRace, controllerClient, serverConnection); } } }.start(); @@ -285,58 +247,25 @@ public class StartController extends Controller implements Observer { - /** - * Function to handle changes in objects we observe. - * We observe LatestMessages. - * @param o The observed object. - * @param arg The {@link Observable#notifyObservers(Object)} parameter. - */ - @Override - public void update(Observable o, Object arg) { - - //Check that we actually have LatestMessages. - if (o instanceof LatestMessages) { - LatestMessages latestMessages = (LatestMessages) o; - - //If we've received all of the xml files, start the race. Only start it if it hasn't already been created. - if (latestMessages.hasAllXMLMessages() && this.visualiserRace == null) { - - //Need to handle it in the javafx thread. - Platform.runLater(() -> { - try { - this.startRace(latestMessages); - - } catch (XMLReaderException | InvalidBoatDataException | InvalidRaceDataException | InvalidRegattaDataException e) { - //We currently don't handle this in meaningful way, as it should never occur. - //If we reach this point it means that malformed XML files were sent. - e.printStackTrace(); - - } - }); - } - } - - } - /** * Show starting information for a race given a socket. * @param socket network source of information */ public void enterLobby(Socket socket) { - startWrapper.setVisible(true); try { - LatestMessages latestMessages = new LatestMessages(); - this.serverConnection = new ServerConnection(socket, latestMessages, RequestToJoinEnum.PARTICIPANT); - this.controllerClient = serverConnection.getControllerClient(); + this.visualiserRaceEvent = new VisualiserRaceEvent(socket, RequestToJoinEnum.PARTICIPANT); + + this.controllerClient = visualiserRaceEvent.getControllerClient(); + + startWrapper.setVisible(true); - //Store a reference to latestMessages so that we can observe it. - latestMessages.addObserver(this); - new Thread(this.serverConnection).start(); + startRace(); } catch (IOException e) { - Logger.getGlobal().log(Level.WARNING, "Could not connection to server.", e); + //TODO should probably let this propagate, so that we only enter this scene if everything works + Logger.getGlobal().log(Level.WARNING, "Could not connect to server.", e); } } diff --git a/racevisionGame/src/main/java/visualiser/model/RaceMap.java b/racevisionGame/src/main/java/visualiser/model/RaceMap.java index 1583e0a5..96e60347 100644 --- a/racevisionGame/src/main/java/visualiser/model/RaceMap.java +++ b/racevisionGame/src/main/java/visualiser/model/RaceMap.java @@ -11,22 +11,22 @@ public class RaceMap { /** * The longitude of the left side of the map. */ - private final double longLeft; + private double longLeft; /** * The longitude of the right side of the map. */ - private final double longRight; + private double longRight; /** * The latitude of the top side of the map. */ - private final double latTop; + private double latTop; /** * The latitude of the bottom side of the map. */ - private final double latBottom; + private double latBottom; /** @@ -143,4 +143,27 @@ public class RaceMap { public void setHeight(int height) { this.height = height; } + + + /** + * Updates the bottom right GPS coordinates of the RaceMap. + * @param bottomRight New bottom right GPS coordinates. + */ + public void setGPSBotRight(GPSCoordinate bottomRight) { + this.latBottom = bottomRight.getLatitude(); + this.longRight = bottomRight.getLongitude(); + } + + + /** + * Updates the top left GPS coordinates of the RaceMap. + * @param topLeft New top left GPS coordinates. + */ + public void setGPSTopLeft(GPSCoordinate topLeft) { + this.latTop = topLeft.getLatitude(); + this.longLeft = topLeft.getLongitude(); + } + + + } diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index fd2ce085..5a0243f6 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -1,8 +1,6 @@ package visualiser.model; -import javafx.scene.Node; -import javafx.scene.image.Image; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.transform.Rotate; @@ -12,6 +10,7 @@ import shared.model.GPSCoordinate; import shared.model.Mark; import shared.model.RaceClock; +import java.util.ArrayList; import java.util.List; /** @@ -36,7 +35,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { /** * The race we read data from and draw. */ - private VisualiserRace visualiserRace; + private VisualiserRaceEvent visualiserRace; private boolean annoName = true; @@ -49,15 +48,15 @@ public class ResizableRaceCanvas extends ResizableCanvas { /** - * Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRace}. + * Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRaceEvent}. * @param visualiserRace The race that data is read from in order to be drawn. */ - public ResizableRaceCanvas(VisualiserRace visualiserRace) { + public ResizableRaceCanvas(VisualiserRaceEvent visualiserRace) { super(); this.visualiserRace = visualiserRace; - RaceDataSource raceData = visualiserRace.getRaceDataSource(); + RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource(); double lat1 = raceData.getMapTopLeft().getLatitude(); double long1 = raceData.getMapTopLeft().getLongitude(); @@ -264,8 +263,8 @@ public class ResizableRaceCanvas extends ResizableCanvas { boat.getCountry(), boat.getCurrentSpeed(), this.map.convertGPS(boat.getCurrentPosition()), - boat.getTimeToNextMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()), - boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()) ); + boat.getTimeToNextMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()), + boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()) ); } @@ -277,7 +276,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { */ private void drawBoats() { - for (VisualiserBoat boat : visualiserRace.getBoats()) { + for (VisualiserBoat boat : new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoats())) { //Draw the boat. drawBoat(boat); @@ -290,7 +289,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { //If the race hasn't started, we set the time since last mark to the current time, to ensure we don't start counting until the race actually starts. if ((boat.getStatus() != BoatStatusEnum.RACING) && (boat.getStatus() == BoatStatusEnum.FINISHED)) { - boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime()); + boat.setTimeAtLastMark(visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()); } //Draw boat label. @@ -402,7 +401,8 @@ public class ResizableRaceCanvas extends ResizableCanvas { * Draws all of the {@link Mark}s on the canvas. */ private void drawMarks() { - for (Mark mark : this.visualiserRace.getMarks()) { + + for (Mark mark : new ArrayList<>(visualiserRace.getVisualiserRaceState().getMarks())) { drawMark(mark); } } @@ -462,7 +462,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { //Calculate the screen coordinates of the boundary. - List boundary = this.visualiserRace.getBoundary(); + List boundary = new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoundary()); double[] xpoints = new double[boundary.size()]; double[] ypoints = new double[boundary.size()]; @@ -486,6 +486,10 @@ public class ResizableRaceCanvas extends ResizableCanvas { */ public void drawRace() { + //Update RaceMap with new GPS values of race. + this.map.setGPSTopLeft(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapTopLeft()); + this.map.setGPSBotRight(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapBottomRight()); + gc.setLineWidth(2); clear(); @@ -520,7 +524,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { gc.setFill(boat.getColor()); //Draw each TrackPoint. - for (TrackPoint point : boat.getTrack()) { + for (TrackPoint point : new ArrayList<>(boat.getTrack())) { //Convert the GPSCoordinate to a screen coordinate. GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate()); diff --git a/racevisionGame/src/main/java/visualiser/model/Sparkline.java b/racevisionGame/src/main/java/visualiser/model/Sparkline.java index 5e802ff9..d8f21f77 100644 --- a/racevisionGame/src/main/java/visualiser/model/Sparkline.java +++ b/racevisionGame/src/main/java/visualiser/model/Sparkline.java @@ -2,6 +2,7 @@ package visualiser.model; import javafx.application.Platform; import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; @@ -23,7 +24,7 @@ public class Sparkline { /** * The race to observe. */ - private VisualiserRace race; + private VisualiserRaceState race; /** * The boats to observe. @@ -58,9 +59,9 @@ public class Sparkline { * @param race The race to listen to. * @param sparklineChart JavaFX LineChart for the sparkline. */ - public Sparkline(VisualiserRace race, LineChart sparklineChart) { + public Sparkline(VisualiserRaceState race, LineChart sparklineChart) { this.race = race; - this.boats = race.getBoats(); + this.boats = new SortedList<>(race.getBoats()); this.legNum = race.getLegCount(); this.sparklineChart = sparklineChart; diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java index c99c85cb..41d0e484 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java @@ -174,28 +174,33 @@ public class VisualiserBoat extends Boat { */ public String getTimeToNextMarkFormatted(ZonedDateTime currentTime) { - //Calculate time delta. - Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark()); + if (getTimeAtLastMark() != null) { + //Calculate time delta. + Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark()); - //Convert to seconds. - long secondsUntil = timeUntil.getSeconds(); + //Convert to seconds. + long secondsUntil = timeUntil.getSeconds(); - //This means the estimated time is in the past, or not racing. - if ((secondsUntil < 0) || (getStatus() != BoatStatusEnum.RACING)) { - return " -"; - } + //This means the estimated time is in the past, or not racing. + if ((secondsUntil < 0) || (getStatus() != BoatStatusEnum.RACING)) { + return " -"; + } - if (secondsUntil <= 60) { - //If less than 1 minute, display seconds only. - return " " + secondsUntil + "s"; + if (secondsUntil <= 60) { + //If less than 1 minute, display seconds only. + return " " + secondsUntil + "s"; - } else { - //Otherwise display minutes and seconds. - long seconds = secondsUntil % 60; - long minutes = (secondsUntil - seconds) / 60; - return String.format(" %dm %ds", minutes, seconds); + } else { + //Otherwise display minutes and seconds. + long seconds = secondsUntil % 60; + long minutes = (secondsUntil - seconds) / 60; + return String.format(" %dm %ds", minutes, seconds); + + } + } else { + return " -"; } } diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java deleted file mode 100644 index 0dc172cd..00000000 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java +++ /dev/null @@ -1,472 +0,0 @@ -package visualiser.model; - -import javafx.animation.AnimationTimer; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.scene.paint.Color; -import network.Messages.BoatLocation; -import network.Messages.BoatStatus; -import network.Messages.Enums.BoatStatusEnum; -import network.Messages.Enums.RaceStatusEnum; -import network.Messages.LatestMessages; -import network.Messages.RaceStatus; -import shared.dataInput.BoatDataSource; -import shared.dataInput.RaceDataSource; -import shared.dataInput.RegattaDataSource; -import shared.model.*; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * The Class used to view the race streamed. - * Has a course, boats, boundaries, etc... - * Observes LatestMessages and updates its state based on new messages. - */ -public class VisualiserRace extends Race implements Runnable { - - - /** - * An observable list of boats in the race. - */ - private final ObservableList boats; - - /** - * An observable list of marker boats in the race. - */ - private ObservableList boatMarkers; - - - /** - * Maps between a Leg to a list of boats, in the order that they finished the leg. - * Used by the Sparkline to ensure it has correct information. - */ - private Map> legCompletionOrder = new HashMap<>(); - - - - /** - * Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and receives events from LatestMessages. - * @param boatDataSource Data source for boat related data (yachts and marker boats). - * @param raceDataSource Data source for race related data (participating boats, legs, etc...). - * @param regattaDataSource Data source for race related data (course name, location, timezone, etc...). - * @param latestMessages The LatestMessages to send events to. - * @param colors A collection of colors used to assign a color to each boat. - */ - public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, List colors) { - - super(boatDataSource, raceDataSource, regattaDataSource, latestMessages); - - - this.boats = FXCollections.observableArrayList(this.generateVisualiserBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), colors)); - - this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values()); - - - //Initialise the leg completion order map. - for (Leg leg : this.legs) { - this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size())); - } - - } - - - /** - * Sets the race data source for this race to a new RaceDataSource. - * Uses the boundary and legs specified by the new RaceDataSource. - * @param raceDataSource The new RaceDataSource to use. - */ - public void setRaceDataSource(RaceDataSource raceDataSource) { - this.raceDataSource = raceDataSource; - - this.boundary = raceDataSource.getBoundary(); - - this.useLegsList(raceDataSource.getLegs()); - } - - /** - * Sets the boat data source for this race to a new BoatDataSource. - * Uses the marker boats specified by the new BoatDataSource. - * @param boatDataSource The new BoatDataSource to use. - */ - public void setBoatDataSource(BoatDataSource boatDataSource) { - this.boatDataSource = boatDataSource; - - this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values()); - } - - /** - * Sets the regatta data source for this race to a new RegattaDataSource. - * @param regattaDataSource The new RegattaDataSource to use. - */ - public void setRegattaDataSource(RegattaDataSource regattaDataSource) { - this.regattaDataSource = regattaDataSource; - } - - - /** - * Returns a list of {@link Mark} boats. - * @return List of mark boats. - */ - public ObservableList getMarks() { - return boatMarkers; - } - - /** - * Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats. - * @param boats The map of Boats describing boats that are potentially in the race. Maps boat sourceID to boat. - * @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating. - * @param colors The list of colors to be used for the boats. - * @return A list of MockBoats that are participating in the race. - */ - private List generateVisualiserBoats(Map boats, List sourceIDs, List colors) { - - List visualiserBoats = new ArrayList<>(sourceIDs.size()); - - //For each sourceID participating... - int colorIndex = 0; - for (int sourceID : sourceIDs) { - - //Get the boat associated with the sourceID. - Boat boat = boats.get(sourceID); - - //Get a color for the boat. - Color color = colors.get(colorIndex); - - //Construct a VisualiserBoat using the Boat and Polars. - VisualiserBoat visualiserBoat = new VisualiserBoat(boat, color); - - visualiserBoats.add(visualiserBoat); - - //Next color. - colorIndex++; - - } - - return visualiserBoats; - - } - - - /** - * Initialise the boats in the race. - * This sets their current leg. - */ - @Override - protected void initialiseBoats() { - - Leg startingLeg = legs.get(0); - - for (VisualiserBoat boat : boats) { - - boat.setCurrentLeg(startingLeg); - boat.setTimeAtLastMark(this.raceClock.getCurrentTime()); - - } - - } - - - /** - * Updates all of the racing boats based on messages received. - * @param boats The list of racing boats. - * @param boatLocationMap A map between boat sourceIDs and BoatLocation messages. - * @param boatStatusMap A map between boat sourceIDs and BoatStatus messages. - */ - private void updateBoats(ObservableList boats, Map boatLocationMap, Map boatStatusMap) { - - for (VisualiserBoat boat : boats) { - BoatLocation boatLocation = boatLocationMap.get(boat.getSourceID()); - BoatStatus boatStatus = boatStatusMap.get(boat.getSourceID()); - updateBoat(boat, boatLocation, boatStatus); - } - } - - - /** - * Updates an individual racing boat based on messages received. - * @param boat The boat to update. - * @param boatLocation The BoatLocation message to use. - * @param boatStatus The BoatStatus message to use. - */ - private void updateBoat(VisualiserBoat boat, BoatLocation boatLocation, BoatStatus boatStatus) { - - if (boatLocation != null && boatStatus != null) { - - //Get the new position. - double latitude = boatLocation.getLatitude(); - double longitude = boatLocation.getLongitude(); - GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude); - - boat.setCurrentPosition(gpsCoordinate); - - //Bearing. - boat.setBearing(boatLocation.getHeading()); - - //Time until next mark. - boat.setEstimatedTimeAtNextMark(raceClock.getLocalTime(boatStatus.getEstTimeAtNextMark())); - - //Speed. - boat.setCurrentSpeed(boatLocation.getBoatSpeedKnots()); - - - //Boat status. - BoatStatusEnum newBoatStatusEnum = boatStatus.getBoatStatus(); - - //If we are changing from non-racing to racing, we need to initialise boat with their time at last mark. - if ((boat.getStatus() != BoatStatusEnum.RACING) && (newBoatStatusEnum == BoatStatusEnum.RACING)) { - boat.setTimeAtLastMark(this.raceClock.getCurrentTime()); - } - - boat.setStatus(newBoatStatusEnum); - - - //Leg. - int legNumber = boatStatus.getLegNumber(); - - if (legNumber >= 1 && legNumber < legs.size()) { - if (boat.getCurrentLeg() != legs.get(legNumber)) { - boatFinishedLeg(boat, legs.get(legNumber)); - } - } - - - //Attempt to add a track point. - if (newBoatStatusEnum == BoatStatusEnum.RACING) { - boat.addTrackPoint(boat.getCurrentPosition(), raceClock.getCurrentTime()); - } - - //Set finish time if boat finished. - if (newBoatStatusEnum == BoatStatusEnum.FINISHED || legNumber == this.legs.size()) { - boat.setTimeFinished(boatLocation.getTime()); - boat.setStatus(BoatStatusEnum.FINISHED); - - } - - } - } - - - /** - * Updates a boat's leg to a specified leg. Also records the order in which the boat passed the leg. - * @param boat The boat to update. - * @param leg The leg to use. - */ - private void boatFinishedLeg(VisualiserBoat boat, Leg leg) { - - //Record order in which boat finished leg. - this.legCompletionOrder.get(boat.getCurrentLeg()).add(boat); - - //Update boat. - boat.setCurrentLeg(leg); - boat.setTimeAtLastMark(this.raceClock.getCurrentTime()); - - } - - - /** - * Updates all of the marker boats based on messages received. - * @param boatMarkers The list of marker boats. - * @param boatLocationMap A map between boat sourceIDs and BoatLocation messages. - * @param boatStatusMap A map between boat sourceIDs and BoatStatus messages. - */ - private void updateMarkers(ObservableList boatMarkers, Map boatLocationMap, Map boatStatusMap) { - - for (Mark mark : boatMarkers) { - BoatLocation boatLocation = boatLocationMap.get(mark.getSourceID()); - updateMark(mark, boatLocation); - } - } - - /** - * Updates an individual marker boat based on messages received. - * @param mark The marker boat to be updated. - * @param boatLocation The message describing the boat's new location. - */ - private void updateMark(Mark mark, BoatLocation boatLocation) { - - if (boatLocation != null) { - - //We only update the boat's position. - double latitude = boatLocation.getLatitude(); - double longitude = boatLocation.getLongitude(); - GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude); - - mark.setPosition(gpsCoordinate); - - } - } - - - /** - * Updates the race status (RaceStatusEnum, wind bearing, wind speed) based on received messages. - * @param raceStatus The RaceStatus message received. - */ - private void updateRaceStatus(RaceStatus raceStatus) { - - //Race status enum. - this.raceStatusEnum = raceStatus.getRaceStatus(); - - //Wind. - this.setWind( - raceStatus.getWindDirection(), - raceStatus.getWindSpeed() ); - - //Current race time. - this.raceClock.setUTCTime(raceStatus.getCurrentTime()); - - } - - - - - /** - * Runnable for the thread. - */ - public void run() { - initialiseBoats(); - startRaceStream(); - } - - - - - /** - * Starts the race. - * This updates the race based on {@link #latestMessages}. - */ - private void startRaceStream() { - - new AnimationTimer() { - - long lastFrameTime = System.currentTimeMillis(); - - @Override - public void handle(long arg0) { - - - //Calculate the frame period. - long currentFrameTime = System.currentTimeMillis(); - long framePeriod = currentFrameTime - lastFrameTime; - - - //Update race status. - if (latestMessages.getRaceStatus() != null) { - updateRaceStatus(latestMessages.getRaceStatus()); - } - - - //Update racing boats. - updateBoats(boats, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap()); - //And their positions (e.g., 5th). - updateBoatPositions(boats); - - - //Update marker boats. - updateMarkers(boatMarkers, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap()); - - - - - if (getRaceStatusEnum() == RaceStatusEnum.FINISHED) { - stop(); - } - - lastFrameTime = currentFrameTime; - - //Increment fps. - incrementFps(framePeriod); - - } - }.start(); - } - - /** - * Update position of boats in race (e.g, 5th), no position if on starting leg or DNF. - * @param boats The list of boats to update. - */ - private void updateBoatPositions(ObservableList boats) { - - //Sort boats. - sortBoatsByPosition(boats); - - //Assign new positions. - for (int i = 0; i < boats.size(); i++) { - VisualiserBoat boat = boats.get(i); - - - if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) { - - boat.setPosition("-"); - - } else { - boat.setPosition(Integer.toString(i + 1)); - } - } - - } - - /** - * Sorts the list of boats by their position within the race. - * @param boats The list of boats in the race. - */ - private void sortBoatsByPosition(ObservableList boats) { - - FXCollections.sort(boats, (a, b) -> { - //Get the difference in leg numbers. - int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber(); - - //If they're on the same leg, we need to compare time to finish leg. - if (legNumberDelta == 0) { - return (int) Duration.between(b.getEstimatedTimeAtNextMark(), a.getEstimatedTimeAtNextMark()).toMillis(); - } else { - return legNumberDelta; - } - - }); - - } - - - - - /** - * Returns the boats participating in the race. - * @return ObservableList of boats participating in the race. - */ - public ObservableList getBoats() { - return boats; - } - - /** - * Returns the order in which boats completed each leg. Maps the leg to a list of boats, ordered by the order in which they finished the leg. - * @return Leg completion order for each leg. - */ - public Map> getLegCompletionOrder() { - return legCompletionOrder; - } - - /** - * Takes an estimated time an event will occur, and converts it to the - * number of seconds before the event will occur. - * - * @param estTimeMillis The estimated time, in milliseconds. - * @param currentTime The current time, in milliseconds. - * @return int difference between time the race started and the estimated time - */ - private int convertEstTime(long estTimeMillis, long currentTime) { - - //Calculate millisecond delta. - long estElapsedMillis = estTimeMillis - currentTime; - - //Convert milliseconds to seconds. - int estElapsedSecs = Math.round(estElapsedMillis / 1000); - - return estElapsedSecs; - - } - -} diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java index 15adc13b..748808b3 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java @@ -1,6 +1,11 @@ package visualiser.model; + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import mock.model.commandFactory.CompositeCommand; import network.Messages.*; import shared.model.RunnableWithFramePeriod; +import visualiser.Commands.VisualiserRaceCommands.VisualiserRaceCommandFactory; import java.util.concurrent.BlockingQueue; import java.util.logging.Level; @@ -8,8 +13,7 @@ import java.util.logging.Logger; /** - * TCP client which receives packets/messages from a race data source - * (e.g., mock source, official source), and exposes them to any observers. + * The controller for race related messages, coming from the server to the client. */ public class VisualiserRaceController implements RunnableWithFramePeriod { @@ -21,31 +25,29 @@ public class VisualiserRaceController implements RunnableWithFramePeriod { /** - * An object containing the set of latest messages to write to. - * Every server frame, VisualiserInput reads messages from its incomingMessages, and write them to this. + * Commands are placed in here, and executed by visualiserRace. */ - private LatestMessages latestMessages; + private CompositeCommand compositeRaceCommand; /** - * Constructs a visualiserInput to convert an incoming stream of messages into LatestMessages. - * - * @param latestMessages Object to place messages in. - * @param incomingMessages The incoming queue of messages. + * The context that created commands operate on. */ - public VisualiserRaceController(LatestMessages latestMessages, BlockingQueue incomingMessages) { - this.latestMessages = latestMessages; - this.incomingMessages = incomingMessages; - } + private VisualiserRaceState visualiserRace; + + /** - * Returns the LatestMessages object, which can be queried for any received race related messages. - * - * @return The LatestMessages object. + * Constructs a visualiserInput to convert an incoming stream of messages into commands. + * @param incomingMessages The incoming queue of messages. + * @param visualiserRace The context to for commands to operate on. + * @param compositeRaceCommand The composite command to place command in. */ - public LatestMessages getLatestMessages() { - return latestMessages; + public VisualiserRaceController(BlockingQueue incomingMessages, VisualiserRaceState visualiserRace, CompositeCommand compositeRaceCommand) { + this.incomingMessages = incomingMessages; + this.compositeRaceCommand = compositeRaceCommand; + this.visualiserRace = visualiserRace; } @@ -56,157 +58,21 @@ public class VisualiserRaceController implements RunnableWithFramePeriod { while (!Thread.interrupted()) { - 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()) { - - - //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"); + AC35Data message = incomingMessages.take(); - 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; - } + Command command = VisualiserRaceCommandFactory.create(message, visualiserRace); + compositeRaceCommand.addCommand(command); + } catch (CommandConstructionException e) { + Logger.getGlobal().log(Level.WARNING, "VisualiserRaceController could not create a command for incoming message."); + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "VisualiserRaceController was interrupted on thread: " + Thread.currentThread() + " while waiting for messages."); + Thread.currentThread().interrupt(); + return; } - //Main loop. - // take message - // create command - // place in command queue - - } } diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceEvent.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceEvent.java new file mode 100644 index 00000000..d5ab9b66 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceEvent.java @@ -0,0 +1,119 @@ +package visualiser.model; + + +import javafx.beans.property.IntegerProperty; +import mock.model.commandFactory.CompositeCommand; +import network.Messages.Enums.RequestToJoinEnum; +import shared.dataInput.EmptyBoatDataSource; +import shared.dataInput.EmptyRaceDataSource; +import shared.dataInput.EmptyRegattaDataSource; +import visualiser.gameController.ControllerClient; +import visualiser.network.ServerConnection; + +import java.io.IOException; +import java.net.Socket; + + +/** + * This class holds a race, and a client's connection to it + */ +public class VisualiserRaceEvent { + + /** + * Our connection to the server. + */ + private ServerConnection serverConnection; + /** + * The thread serverConnection is running on. + */ + private Thread serverConnectionThread; + + + + /** + * The race object which describes the currently occurring race. + */ + private VisualiserRaceState visualiserRaceState; + + + /** + * The service for updating the {@link #visualiserRaceState}. + */ + private VisualiserRaceService visualiserRaceService; + /** + * The thread {@link #visualiserRaceService} is running on. + */ + private Thread visualiserRaceServiceThread; + + + + /** + * Creates a visualiser race event, with a given socket and request type. + * @param socket The socket to connect to. + * @param requestType The type of {@link network.Messages.RequestToJoin} to make. + * @throws IOException Thrown if there is a problem with the socket. + */ + public VisualiserRaceEvent(Socket socket, RequestToJoinEnum requestType) throws IOException { + + this.visualiserRaceState = new VisualiserRaceState(new EmptyRaceDataSource(), new EmptyRegattaDataSource(), new EmptyBoatDataSource()); + + + CompositeCommand raceCommands = new CompositeCommand(); + this.visualiserRaceService = new VisualiserRaceService(raceCommands, visualiserRaceState); + + this.visualiserRaceServiceThread = new Thread(visualiserRaceService, "VisualiserRaceEvent()->VisualiserRaceService thread " + visualiserRaceService); + this.visualiserRaceServiceThread.start(); + + + this.serverConnection = new ServerConnection(socket, visualiserRaceState, raceCommands, requestType); + this.serverConnectionThread = new Thread(serverConnection, "StartController.enterLobby()->serverConnection thread " + serverConnection); + this.serverConnectionThread.start(); + + + } + + + /** + * Returns the state of the race. + * @return The state of the race. + */ + public VisualiserRaceState getVisualiserRaceState() { + return visualiserRaceState; + } + + + /** + * Returns the controller client, which writes BoatAction messages to the outgoing queue. + * @return The ControllerClient. + */ + public ControllerClient getControllerClient() { + return serverConnection.getControllerClient(); + } + + /** + * Returns the connection to server. + * @return Connection to server. + */ + public ServerConnection getServerConnection() { + return serverConnection; + } + + /** + * Returns the framerate property of the race. + * @return Framerate property of race. + */ + public IntegerProperty getFrameRateProperty() { + return visualiserRaceService.getFrameRateProperty(); + } + + + + /** + * Terminates the server connection and race service. + */ + public void terminate() { + this.serverConnectionThread.interrupt(); + + this.visualiserRaceServiceThread.interrupt(); + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceService.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceService.java new file mode 100644 index 00000000..21be26d9 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceService.java @@ -0,0 +1,93 @@ +package visualiser.model; + +import javafx.beans.property.IntegerProperty; +import mock.model.commandFactory.CompositeCommand; +import shared.model.FrameRateTracker; +import shared.model.RunnableWithFramePeriod; + + +/** + * Handles updating a {@link VisualiserRaceState} with incoming commands. + */ +public class VisualiserRaceService implements RunnableWithFramePeriod { + + + /** + * The race state to update. + */ + private VisualiserRaceState visualiserRaceState; + + + /** + * A composite commands to execute to update the race. + */ + private CompositeCommand raceCommands; + + + /** + * Used to track the framerate of the "simulation". + */ + private FrameRateTracker frameRateTracker; + + + + + + + /** + * Constructs a visualiser race which models a yacht race, and is modified by CompositeCommand. + * @param raceCommands A composite commands to execute to update the race. + * @param visualiserRaceState The race state to update. + */ + public VisualiserRaceService(CompositeCommand raceCommands, VisualiserRaceState visualiserRaceState) { + this.raceCommands = raceCommands; + this.visualiserRaceState = visualiserRaceState; + + this.frameRateTracker = new FrameRateTracker(); + } + + + + /** + * Returns the CompositeCommand executed by the race. + * @return CompositeCommand executed by race. + */ + public CompositeCommand getRaceCommands() { + return raceCommands; + } + + + + + + @Override + public void run() { + + long previousFrameTime = System.currentTimeMillis(); + + while (!Thread.interrupted()) { + + long currentFrameTime = System.currentTimeMillis(); + + waitForFramePeriod(previousFrameTime, currentFrameTime, 16); + + previousFrameTime = currentFrameTime; + + + raceCommands.execute(); + + } + + frameRateTracker.stop(); + + } + + + /** + * Returns the framerate property of the race. + * @return Framerate property of race. + */ + public IntegerProperty getFrameRateProperty() { + return frameRateTracker.fpsProperty(); + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java new file mode 100644 index 00000000..4dc6519d --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java @@ -0,0 +1,411 @@ +package visualiser.model; + + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.paint.Color; +import network.Messages.Enums.BoatStatusEnum; +import shared.dataInput.BoatDataSource; +import shared.dataInput.RaceDataSource; +import shared.dataInput.RegattaDataSource; +import shared.exceptions.BoatNotFoundException; +import shared.exceptions.MarkNotFoundException; +import shared.model.*; + +import java.time.Duration; +import java.util.*; + + +/** + * This class contains all of the state of a race on the client (visualiser) side. + */ +public class VisualiserRaceState extends RaceState { + + + /** + * A list of boats in the race. + */ + private ObservableList boats; + + /** + * The source ID of the boat assigned to the player. + * 0 if no boat has been assigned. + */ + private int playerBoatID; + + + /** + * Maps between a Leg to a list of boats, in the order that they finished the leg. + * Used by the Sparkline to ensure it has correct information. + * TODO BUG: if we receive a race.xml file during the race, then we need to add/remove legs to this, without losing information. + */ + private Map> legCompletionOrder; + + + + + /** + * An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor. + */ + private List unassignedColors = new ArrayList<>(Arrays.asList( + Color.BLUEVIOLET, + Color.BLACK, + Color.RED, + Color.ORANGE, + Color.DARKOLIVEGREEN, + Color.LIMEGREEN, + Color.PURPLE, + Color.DARKGRAY, + Color.YELLOW + //TODO may need to add more colors. + )); + + + + + /** + * Constructs a visualiser race which models a yacht race. + */ + public VisualiserRaceState(RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, BoatDataSource boatDataSource) { + + this.boats = FXCollections.observableArrayList(); + + this.playerBoatID = 0; + + this.legCompletionOrder = new HashMap<>(); + + + setRaceDataSource(raceDataSource); + setRegattaDataSource(regattaDataSource); + setBoatDataSource(boatDataSource); + + } + + + + + /** + * Sets the race data source for this race to a new RaceDataSource. + * Uses the boundary and legs specified by the new RaceDataSource. + * @param raceDataSource The new RaceDataSource to use. + */ + public void setRaceDataSource(RaceDataSource raceDataSource) { + super.setRaceDataSource(raceDataSource); + + if (getBoatDataSource() != null) { + this.generateVisualiserBoats(this.boats, getBoatDataSource().getBoats(), raceDataSource.getParticipants(), unassignedColors); + } + + useLegsList(raceDataSource.getLegs()); + } + + /** + * Sets the boat data source for this race to a new BoatDataSource. + * Uses the marker boats specified by the new BoatDataSource. + * @param boatDataSource The new BoatDataSource to use. + */ + public void setBoatDataSource(BoatDataSource boatDataSource) { + super.setBoatDataSource(boatDataSource); + + if (getRaceDataSource() != null) { + this.generateVisualiserBoats(this.boats, boatDataSource.getBoats(), getRaceDataSource().getParticipants(), unassignedColors); + } + } + + + /** + * Sets the regatta data source for this race to a new RegattaDataSource. + * @param regattaDataSource The new RegattaDataSource to use. + */ + public void setRegattaDataSource(RegattaDataSource regattaDataSource) { + super.setRegattaDataSource(regattaDataSource); + + } + + + /** + * See {@link RaceState#useLegsList(List)}. + * Also initialises the {@link #legCompletionOrder} map. + * @param legs The new list of legs to use. + */ + @Override + public void useLegsList(List legs) { + super.useLegsList(legs); + + //Initialise the leg completion order map. + for (Leg leg : getLegs()) { + this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size())); + } + } + + + + /** + * Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats. + * This will add VisualiserBoats for newly participating sourceID, and remove VisualiserBoats for any participating sourceIDs that have been removed. + * + * @param existingBoats The visualiser boats that already exist in the race. This will be populated when we receive a new race.xml or boats.xml. + * @param boats The map of {@link Boat}s describing boats that are potentially in the race. Maps boat sourceID to boat. + * @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating. + * @param colors The list of unassignedColors to be used for the boats. + */ + private void generateVisualiserBoats(ObservableList existingBoats, Map boats, List sourceIDs, List colors) { + + //Remove any VisualiserBoats that are no longer participating. + for (VisualiserBoat boat : new ArrayList<>(existingBoats)) { + + //Boat no longer is participating. + if (!sourceIDs.contains(boat.getSourceID())) { + //Return their colors to the color list. + colors.add(boat.getColor()); + + //Remove boat. + existingBoats.remove(boat); + } + + } + + //Get source IDs of already existing boats. + List existingBoatIDs = new ArrayList<>(); + for (VisualiserBoat boat : existingBoats) { + existingBoatIDs.add(boat.getSourceID()); + } + + //Get source IDs of only newly participating boats. + List newBoatIDs = new ArrayList<>(sourceIDs); + newBoatIDs.removeAll(existingBoatIDs); + + //Create VisualiserBoat for newly participating boats. + for (Integer sourceID : newBoatIDs) { + + if (boats.containsKey(sourceID)) { + + VisualiserBoat boat = new VisualiserBoat( + boats.get(sourceID), + colors.remove(colors.size() - 1));//TODO potential bug: not enough colors for boats. + + boat.setCurrentLeg(getLegs().get(0)); + + existingBoats.add(boat); + + } + + } + + setPlayerBoat(); + + + } + + + /** + * Sets the boat the player has been assigned to as belonging to them. + */ + private void setPlayerBoat() { + + if (getPlayerBoatID() != 0) { + + for (VisualiserBoat boat : getBoats()) { + + if (boat.getSourceID() == getPlayerBoatID()) { + boat.setClientBoat(true); + } + + } + + } + + } + + + /** + * Initialise the boats in the race. + * This sets their current leg. + */ + @Override + protected void initialiseBoats() { + + Leg startingLeg = getLegs().get(0); + + for (VisualiserBoat boat : boats) { + + boat.setCurrentLeg(startingLeg); + boat.setTimeAtLastMark(getRaceClock().getCurrentTime()); + boat.setCurrentPosition(new GPSCoordinate(0, 0)); + + } + + } + + + + + /** + * Update position of boats in race (e.g, 5th), no position if on starting leg or DNF. + * @param boats The list of boats to update. + */ + public void updateBoatPositions(List boats) { + + //Sort boats. + sortBoatsByPosition(boats); + + //Assign new positions. + for (int i = 0; i < boats.size(); i++) { + VisualiserBoat boat = boats.get(i); + + + if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) { + + boat.setPosition("-"); + + } else { + boat.setPosition(Integer.toString(i + 1)); + } + } + + } + + + /** + * Sorts the list of boats by their position within the race. + * @param boats The list of boats in the race. + */ + private void sortBoatsByPosition(List boats) { + + boats.sort((a, b) -> { + //Get the difference in leg numbers. + int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber(); + + //If they're on the same leg, we need to compare time to finish leg. + if (legNumberDelta == 0) { + + //These are potentially null until we receive our first RaceStatus containing BoatStatuses. + if ((a.getEstimatedTimeAtNextMark() != null) && (b.getEstimatedTimeAtNextMark() != null)) { + + return (int) Duration.between( + b.getEstimatedTimeAtNextMark(), + a.getEstimatedTimeAtNextMark() ).toMillis(); + + } + + } + + return legNumberDelta; + + }); + + } + + + + + /** + * Returns the boats participating in the race. + * @return List of boats participating in the race. + */ + public ObservableList getBoats() { + return boats; + } + + + /** + * Returns a boat by sourceID. + * @param sourceID The source ID the boat. + * @return The boat. + * @throws BoatNotFoundException Thrown if there is no boat with the specified sourceID. + */ + public VisualiserBoat getBoat(int sourceID) throws BoatNotFoundException { + + for (VisualiserBoat boat : boats) { + + if (boat.getSourceID() == sourceID) { + return boat; + } + + } + + throw new BoatNotFoundException("Boat with sourceID: " + sourceID + " was not found."); + } + + /** + * Returns whether or not there exists a {@link VisualiserBoat} with the given source ID. + * @param sourceID SourceID of VisualiserBoat. + * @return True if VisualiserBoat exists, false otherwise. + */ + public boolean isVisualiserBoat(int sourceID) { + + try { + getBoat(sourceID); + return true; + } catch (BoatNotFoundException e) { + return false; + } + } + + + /** + * Returns a mark by sourceID. + * @param sourceID The source ID the mark. + * @return The mark. + * @throws MarkNotFoundException Thrown if there is no mark with the specified sourceID. + */ + public Mark getMark(int sourceID) throws MarkNotFoundException { + + for (Mark mark : getMarks()) { + + if (mark.getSourceID() == sourceID) { + return mark; + } + + } + + throw new MarkNotFoundException("Mark with sourceID: " + sourceID + " was not found."); + } + + + /** + * Returns whether or not there exists a {@link Mark} with the given source ID. + * @param sourceID SourceID of mark. + * @return True if mark exists, false otherwise. + */ + public boolean isMark(int sourceID) { + + try { + getMark(sourceID); + return true; + } catch (MarkNotFoundException e) { + return false; + } + } + + + + + /** + * Returns the order in which boats completed each leg. Maps the leg to a list of boats, ordered by the order in which they finished the leg. + * @return Leg completion order for each leg. + */ + public Map> getLegCompletionOrder() { + return legCompletionOrder; + } + + + /** + * Gets the source ID of the player's boat. 0 if not assigned. + * @return Players boat source ID. + */ + public int getPlayerBoatID() { + return playerBoatID; + } + + /** + * sets the source ID of the player's boat. 0 if not assigned. + * @param playerBoatID Players boat source ID. + */ + public void setPlayerBoatID(int playerBoatID) { + this.playerBoatID = playerBoatID; + setPlayerBoat(); + } + + +} diff --git a/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java b/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java index 4d22fc23..72d49c4a 100644 --- a/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java +++ b/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java @@ -147,11 +147,27 @@ public class ConnectionToServer implements RunnableWithFramePeriod { //Send them the source ID. RequestToJoin requestToJoin = new RequestToJoin(requestType); - outgoingMessages.put(requestToJoin); + send(requestToJoin); connectionState = ConnectionToServerState.REQUEST_SENT; } + /** + * Sends a given message to the server, via the {@link #outgoingMessages} queue. + * @param message Message to send. + * @throws InterruptedException Thrown if thread is interrupted while sending message. + */ + public void send(AC35Data message) throws InterruptedException { + outgoingMessages.put(message); + } + + /** + * Returns the type of join request that was made. + * @return Type of join request made. + */ + public RequestToJoinEnum getRequestType() { + return requestType; + } } diff --git a/racevisionGame/src/main/java/visualiser/network/ServerConnection.java b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java index 35b25727..ed540566 100644 --- a/racevisionGame/src/main/java/visualiser/network/ServerConnection.java +++ b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java @@ -2,18 +2,20 @@ package visualiser.network; import mock.model.commandFactory.Command; +import mock.model.commandFactory.CompositeCommand; import network.MessageRouters.MessageRouter; import network.Messages.AC35Data; import network.Messages.Enums.MessageType; import network.Messages.Enums.RequestToJoinEnum; -import network.Messages.JoinAcceptance; import network.Messages.LatestMessages; import network.StreamRelated.MessageDeserialiser; import network.StreamRelated.MessageSerialiser; import shared.model.RunnableWithFramePeriod; +import visualiser.model.VisualiserRaceEvent; import visualiser.model.VisualiserRaceController; import visualiser.enums.ConnectionToServerState; import visualiser.gameController.ControllerClient; +import visualiser.model.VisualiserRaceState; import java.io.IOException; import java.net.Socket; @@ -23,7 +25,7 @@ import java.util.logging.Level; import java.util.logging.Logger; /** - * This class handles the client-server connection handshake, and creation of VisualiserInput and ControllerClient. + * This class handles the client-server connection handshake. */ public class ServerConnection implements RunnableWithFramePeriod { @@ -32,16 +34,8 @@ public class ServerConnection implements RunnableWithFramePeriod { */ 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. - */ - private LatestMessages latestMessages; /** @@ -122,6 +116,16 @@ public class ServerConnection implements RunnableWithFramePeriod { private Thread heartBeatControllerThread; + /** + * This is the race we are modelling. + */ + private VisualiserRaceState visualiserRaceState; + + /** + * The CompositeCommand to place race commands in. + */ + private CompositeCommand raceCommands; + /** * Used to convert incoming messages into a race snapshot. @@ -130,7 +134,7 @@ public class ServerConnection implements RunnableWithFramePeriod { /** * The thread {@link #visualiserRaceController} runs on. */ - private Thread visualiserInputThread; + private Thread visualiserRaceControllerThread; @@ -138,13 +142,15 @@ public class ServerConnection implements RunnableWithFramePeriod { /** * Creates a server connection, using a given socket. * @param socket The socket which connects to the client. - * @param latestMessages Latest race snapshot to send to client. + * @param visualiserRaceState The race for the {@link VisualiserRaceController} to send commands to. + * @param raceCommands The CompositeCommand to place race commands in. * @param requestType The type of join request to make. * @throws IOException Thrown if there is a problem with the client socket. */ - public ServerConnection(Socket socket, LatestMessages latestMessages, RequestToJoinEnum requestType) throws IOException { + public ServerConnection(Socket socket, VisualiserRaceState visualiserRaceState, CompositeCommand raceCommands, RequestToJoinEnum requestType) throws IOException { this.socket = socket; - this.latestMessages = latestMessages; + this.visualiserRaceState = visualiserRaceState; + this.raceCommands = raceCommands; createMessageSerialiser(socket); createMessageDeserialiser(socket); @@ -205,6 +211,15 @@ public class ServerConnection implements RunnableWithFramePeriod { this.connectionToServerControllerThread.start(); } + /** + * Removes connection message related routes from the router. + * This is called after the client-server connection is properly established, so that any future (erroneous) connection messages get ignored. + */ + private void removeConnectionRoutes() { + this.messageRouter.removeRoute(MessageType.JOIN_ACCEPTANCE); + this.messageRouter.removeRoute(MessageType.REQUEST_TO_JOIN); + } + /** * Creates the {@link #messageSerialiser} and starts its thread. @@ -234,7 +249,7 @@ public class ServerConnection implements RunnableWithFramePeriod { /** - * Creates the {@link #heartBeatService} and {@link #heartBeatController} and starts its thread. + * Creates the {@link #heartBeatService} and {@link #heartBeatController} and starts their threads. */ private void createHeartBeatService() { @@ -260,14 +275,16 @@ public class ServerConnection implements RunnableWithFramePeriod { } + /** + * Creates the {@link #visualiserRaceController} and starts its thread. + */ + private void createVisualiserRaceController() { - private void createVisualiserRace() { + //VisualiserRaceController receives messages, and places commands on the race's command queue. BlockingQueue incomingMessages = new LinkedBlockingQueue<>(); + this.visualiserRaceController = new VisualiserRaceController(incomingMessages, visualiserRaceState, raceCommands); - this.visualiserRaceController = new VisualiserRaceController(latestMessages, incomingMessages); - this.visualiserInputThread = new Thread(visualiserRaceController, "ServerConnection()->VisualiserInput thread " + visualiserRaceController); - this.visualiserInputThread.start(); //Routes. this.messageRouter.addRoute(MessageType.BOATLOCATION, incomingMessages); @@ -281,15 +298,19 @@ public class ServerConnection implements RunnableWithFramePeriod { this.messageRouter.addRoute(MessageType.YACHTEVENTCODE, incomingMessages); this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages); this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages); - this.messageRouter.removeDefaultRoute(); + this.messageRouter.addRoute(MessageType.ASSIGN_PLAYER_BOAT, incomingMessages); + this.messageRouter.removeDefaultRoute(); //We no longer want to keep un-routed messages. - //TODO create VisualiserRace here or somewhere else? + //Start the above on a new thread. - } + this.visualiserRaceControllerThread = new Thread(visualiserRaceController, "ServerConnection()->VisualiserRaceController thread " + visualiserRaceController); + this.visualiserRaceControllerThread.start(); + } + //TODO create input controller here. RaceController should query for it, if it exists. private void createPlayerInputController() { this.messageRouter.addRoute(MessageType.BOATACTION, messageSerialiser.getMessagesToSend()); @@ -342,16 +363,17 @@ public class ServerConnection implements RunnableWithFramePeriod { */ private void connected() { - JoinAcceptance joinAcceptance = connectionToServer.getJoinAcceptance(); - - allocatedSourceID = joinAcceptance.getSourceID(); + createHeartBeatService(); + createVisualiserRaceController(); - createHeartBeatService(); + if (connectionToServer.getRequestType() == RequestToJoinEnum.PARTICIPANT) { + createPlayerInputController(); + } - createVisualiserRace(); - createPlayerInputController(); + //We no longer want connection messages to be accepted. + removeConnectionRoutes(); //We interrupt as this thread's run() isn't needed anymore. @@ -403,13 +425,7 @@ public class ServerConnection implements RunnableWithFramePeriod { 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; - } + /** @@ -447,10 +463,12 @@ public class ServerConnection implements RunnableWithFramePeriod { } - if (this.visualiserInputThread != null) { - this.visualiserInputThread.interrupt(); + if (this.visualiserRaceControllerThread != null) { + this.visualiserRaceControllerThread.interrupt(); } - //TODO visualiser race? + + + //TODO input controller? } diff --git a/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml b/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml index 0b2b6a00..9295dc07 100644 --- a/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml +++ b/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml @@ -49,7 +49,7 @@ - + diff --git a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml index 159d725c..e6544cad 100644 --- a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml +++ b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml @@ -3,9 +3,25 @@ - - + + + + + + + + + + + + + + + + + + @@ -87,8 +103,8 @@ - - + + diff --git a/racevisionGame/src/test/java/mock/model/MockRaceTest.java b/racevisionGame/src/test/java/mock/model/MockRaceTest.java index 402e37f0..4f3f7705 100644 --- a/racevisionGame/src/test/java/mock/model/MockRaceTest.java +++ b/racevisionGame/src/test/java/mock/model/MockRaceTest.java @@ -29,13 +29,12 @@ public class MockRaceTest { RaceDataSource raceDataSource = RaceXMLReaderTest.createRaceDataSource(); RegattaDataSource regattaDataSource = RegattaXMLReaderTest.createRegattaDataSource(); - LatestMessages latestMessages = new LatestMessages(); Polars polars = PolarParserTest.createPolars(); WindGenerator windGenerator = new ConstantWindGenerator(Bearing.fromDegrees(230), 10); - MockRace mockRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, polars, Constants.RaceTimeScale, windGenerator); + MockRace mockRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, polars, Constants.RaceTimeScale, windGenerator); return mockRace; diff --git a/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java index 3a48c096..2193eb7a 100644 --- a/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java +++ b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java @@ -8,16 +8,11 @@ import network.Messages.BoatAction; import network.Messages.Enums.BoatActionEnum; import org.junit.Before; import org.junit.Test; -import org.mockito.Mock; import shared.exceptions.InvalidBoatDataException; import shared.exceptions.InvalidRaceDataException; import shared.exceptions.InvalidRegattaDataException; import shared.model.Bearing; -import shared.model.Boat; -import shared.model.Race; -import visualiser.model.VisualiserRace; -import static org.mockito.Mockito.when; import static org.testng.Assert.*; import static org.mockito.Mockito.mock; diff --git a/racevisionGame/src/test/java/network/MessageDecoders/JoinAcceptanceDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/JoinAcceptanceDecoderTest.java index 1e3a5c3d..6c287dd6 100644 --- a/racevisionGame/src/test/java/network/MessageDecoders/JoinAcceptanceDecoderTest.java +++ b/racevisionGame/src/test/java/network/MessageDecoders/JoinAcceptanceDecoderTest.java @@ -64,7 +64,7 @@ public class JoinAcceptanceDecoderTest { */ @Test public void joinSuccessSourceIDTest() throws Exception { - responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL, 12345); + responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, 12345); } /** @@ -73,26 +73,9 @@ public class JoinAcceptanceDecoderTest { */ @Test public void joinSuccessNoSourceIDTest() throws Exception { - responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL, 0); + responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, 0); } - /** - * Tests if a participants full message can be encoded and decoded correctly. - * @throws Exception if test fails. - */ - @Test - public void participantFullTest() throws Exception { - responseTypeTest(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, 0); - } - - /** - * Tests if a ghosts full message can be encoded and decoded correctly. - * @throws Exception if test fails. - */ - @Test - public void ghostFullTest() throws Exception { - responseTypeTest(JoinAcceptanceEnum.GHOST_PARTICIPANTS_FULL, 0); - } /** * Tests if a server full message can be encoded and decoded correctly. diff --git a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java index fae94d1b..64fdfcb5 100644 --- a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java +++ b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java @@ -7,13 +7,15 @@ import network.Messages.Enums.RequestToJoinEnum; import network.Messages.JoinAcceptance; import org.junit.Before; import org.junit.Test; -import visualiser.Commands.ConnectionToServerCommands.JoinSuccessfulCommand; -import visualiser.Commands.ConnectionToServerCommands.RaceParticipantsFullCommand; +import visualiser.Commands.ConnectionToServerCommands.JoinSuccessParticipantCommand; +import visualiser.Commands.ConnectionToServerCommands.JoinFailureCommand; import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand; import visualiser.enums.ConnectionToServerState; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; import static org.junit.Assert.*; @@ -50,7 +52,7 @@ public class ConnectionToServerParticipantTest { public void expectRequestSent() throws Exception { //Need to wait for connection thread to execute commands. - Thread.sleep(20); + Thread.sleep(250); assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState()); } @@ -64,9 +66,14 @@ public class ConnectionToServerParticipantTest { public void interruptTimedOut() throws Exception { //Need to wait for connection thread to execute commands. - Thread.sleep(20); + Thread.sleep(250); + + //Disable logging as we know this will log but we don't care. + Logger.getGlobal().setLevel(Level.OFF); connectionToServerThread.interrupt(); + Logger.getGlobal().setLevel(null); + connectionToServerThread.join(); assertEquals(ConnectionToServerState.TIMED_OUT, connectionToServer.getConnectionState()); @@ -80,20 +87,20 @@ public class ConnectionToServerParticipantTest { @Test public void sendJoinSuccessCommand() throws Exception { int sourceID = 123; - JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID); + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, sourceID); - Command command = new JoinSuccessfulCommand(joinAcceptance, connectionToServer); + Command command = new JoinSuccessParticipantCommand(joinAcceptance, connectionToServer); incomingCommands.put(command); //Need to wait for connection thread to execute commands. - Thread.sleep(20); + Thread.sleep(250); assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState()); assertTrue(connectionToServer.getJoinAcceptance() != null); assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); assertNotEquals(0, connectionToServer.getJoinAcceptance().getSourceID()); - assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, connectionToServer.getJoinAcceptance().getAcceptanceType()); } @@ -104,21 +111,21 @@ public class ConnectionToServerParticipantTest { * @throws Exception On error. */ @Test - public void sendRaceParticipantsFullCommand() throws Exception { + public void sendJoinFailureCommand() throws Exception { int sourceID = 0; - JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, sourceID); + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_FAILURE, sourceID); - Command command = new RaceParticipantsFullCommand(joinAcceptance, connectionToServer); + Command command = new JoinFailureCommand(joinAcceptance, connectionToServer); incomingCommands.put(command); //Need to wait for connection thread to execute commands. - Thread.sleep(20); + Thread.sleep(250); assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState()); assertTrue(connectionToServer.getJoinAcceptance() != null); assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); - assertEquals(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + assertEquals(JoinAcceptanceEnum.JOIN_FAILURE, connectionToServer.getJoinAcceptance().getAcceptanceType()); } @@ -136,7 +143,7 @@ public class ConnectionToServerParticipantTest { incomingCommands.put(command); //Need to wait for connection thread to execute commands. - Thread.sleep(20); + Thread.sleep(250); assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState()); assertTrue(connectionToServer.getJoinAcceptance() != null); diff --git a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java index 8b69c861..c381dc88 100644 --- a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java +++ b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java @@ -7,8 +7,7 @@ import network.Messages.Enums.RequestToJoinEnum; import network.Messages.JoinAcceptance; import org.junit.Before; import org.junit.Test; -import visualiser.Commands.ConnectionToServerCommands.JoinSuccessfulCommand; -import visualiser.Commands.ConnectionToServerCommands.RaceParticipantsFullCommand; +import visualiser.Commands.ConnectionToServerCommands.JoinSuccessParticipantCommand; import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand; import visualiser.enums.ConnectionToServerState; @@ -50,7 +49,7 @@ public class ConnectionToServerSpectatorTest { public void expectRequestSent() throws Exception { //Need to wait for connection thread to execute commands. - Thread.sleep(20); + Thread.sleep(250); assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState()); } @@ -63,19 +62,19 @@ public class ConnectionToServerSpectatorTest { @Test public void sendJoinSuccessCommand() throws Exception { int sourceID = 0; - JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID); + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, sourceID); - Command command = new JoinSuccessfulCommand(joinAcceptance, connectionToServer); + Command command = new JoinSuccessParticipantCommand(joinAcceptance, connectionToServer); incomingCommands.put(command); //Need to wait for connection thread to execute commands. - Thread.sleep(20); + Thread.sleep(250); assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState()); assertTrue(connectionToServer.getJoinAcceptance() != null); assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); - assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, connectionToServer.getJoinAcceptance().getAcceptanceType()); } @@ -95,7 +94,7 @@ public class ConnectionToServerSpectatorTest { incomingCommands.put(command); //Need to wait for connection thread to execute commands. - Thread.sleep(20); + Thread.sleep(250); assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState()); assertTrue(connectionToServer.getJoinAcceptance() != null); From 175fb11178e0340dd572f6c1014c26c5fd5bab30 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Mon, 14 Aug 2017 01:02:58 +1200 Subject: [PATCH 08/16] Javadoc fixes. #story[1095] --- .../ConnectionToServerCommands/JoinFailureCommand.java | 2 +- .../src/main/java/visualiser/model/VisualiserRaceState.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java index 283d6fc2..560509a3 100644 --- a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java @@ -7,7 +7,7 @@ import visualiser.network.ConnectionToServer; /** - * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#RACE_PARTICIPANTS_FULL} {@link JoinAcceptance} message is received. + * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_FAILURE} {@link JoinAcceptance} message is received. */ public class JoinFailureCommand implements Command { diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java index 4dc6519d..faefa467 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java @@ -61,10 +61,11 @@ public class VisualiserRaceState extends RaceState { )); - - /** * Constructs a visualiser race which models a yacht race. + * @param raceDataSource The raceDataSource to initialise with. + * @param regattaDataSource The regattaDataSource to initialise with. + * @param boatDataSource The boatDataSource to initialise with. */ public VisualiserRaceState(RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, BoatDataSource boatDataSource) { From c9875f3987308969c2baa2adfe3f238dc1288569 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 15 Aug 2017 14:37:36 +1200 Subject: [PATCH 09/16] RaceLogic no longer uses AnimationTimer for its main loop (since that ran in javafx thread). --- .../src/main/java/mock/model/RaceLogic.java | 58 +++++++------------ .../java/visualiser/model/VisualiserBoat.java | 4 +- 2 files changed, 24 insertions(+), 38 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/RaceLogic.java b/racevisionGame/src/main/java/mock/model/RaceLogic.java index 7b314d0f..418ade17 100644 --- a/racevisionGame/src/main/java/mock/model/RaceLogic.java +++ b/racevisionGame/src/main/java/mock/model/RaceLogic.java @@ -40,20 +40,23 @@ public class RaceLogic implements RunnableWithFramePeriod { @Override public void run() { race.initialiseBoats(); - this.countdownTimer.start(); + + countdown(); + + raceLoop(); } /** * Countdown timer until race starts. */ - protected AnimationTimer countdownTimer = new AnimationTimer() { + private void countdown() { + long previousFrameTime = System.currentTimeMillis(); - long currentTime = System.currentTimeMillis(); + while (race.getRaceStatusEnum() != RaceStatusEnum.STARTED) { - @Override - public void handle(long arg0) { + long currentTime = System.currentTimeMillis(); //Update race time. race.updateRaceTime(currentTime); @@ -72,47 +75,28 @@ public class RaceLogic implements RunnableWithFramePeriod { race.changeWindDirection(); - - if (race.getRaceStatusEnum() == RaceStatusEnum.STARTED) { race.setBoatsStatusToRacing(); - raceTimer.start(); - this.stop(); } - //Update the animations timer's time. - currentTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentTime, 50); + previousFrameTime = currentTime; + } - }; + } /** * Timer that runs for the duration of the race, until all boats finish. */ - private AnimationTimer raceTimer = new AnimationTimer() { + private void raceLoop() { - /** - * Start time of loop, in milliseconds. - */ - long timeRaceStarted = System.currentTimeMillis(); + long previousFrameTime = System.currentTimeMillis(); - /** - * Current time during a loop iteration. - */ - long currentTime = System.currentTimeMillis(); - - /** - * The time of the previous frame, in milliseconds. - */ - long lastFrameTime = timeRaceStarted; - - long framePeriod = currentTime - lastFrameTime; - - @Override - public void handle(long arg0) { + while (race.getRaceStatusEnum() != RaceStatusEnum.FINISHED) { //Get the current time. - currentTime = System.currentTimeMillis(); + long currentTime = System.currentTimeMillis(); //Execute commands from clients. commands.execute(); @@ -124,7 +108,7 @@ public class RaceLogic implements RunnableWithFramePeriod { if (race.getNumberOfActiveBoats() != 0) { //Get the time period of this frame. - framePeriod = currentTime - lastFrameTime; + long framePeriod = currentTime - previousFrameTime; //For each boat, we update its position, and generate a BoatLocationMessage. for (MockBoat boat : race.getBoats()) { @@ -141,7 +125,6 @@ public class RaceLogic implements RunnableWithFramePeriod { //Otherwise, the race is over! raceFinished.start(); race.setRaceStatusEnum(RaceStatusEnum.FINISHED); - this.stop(); } if (race.getNumberOfActiveBoats() != 0) { @@ -152,10 +135,13 @@ public class RaceLogic implements RunnableWithFramePeriod { server.parseSnapshot(); //Update the last frame time. - this.lastFrameTime = currentTime; + previousFrameTime = currentTime; } + + waitForFramePeriod(previousFrameTime, currentTime, 50); + previousFrameTime = currentTime; } - }; + } /** * Broadcast that the race has finished. diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java index 41d0e484..ce966b02 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java @@ -174,7 +174,7 @@ public class VisualiserBoat extends Boat { */ public String getTimeToNextMarkFormatted(ZonedDateTime currentTime) { - if (getTimeAtLastMark() != null) { + if ((getTimeAtLastMark() != null) && (currentTime != null)) { //Calculate time delta. Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark()); @@ -213,7 +213,7 @@ public class VisualiserBoat extends Boat { */ public String getTimeSinceLastMarkFormatted(ZonedDateTime currentTime) { - if (getTimeAtLastMark() != null) { + if ((getTimeAtLastMark() != null) && (currentTime != null)) { //Calculate time delta. Duration timeSince = Duration.between(getTimeAtLastMark(), currentTime); From 5ae8393126efa6cd58974331d440a580a0480a3f Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 15 Aug 2017 17:13:12 +1200 Subject: [PATCH 10/16] Fixed null pointer exception when a boat finishes the race. Boats now default to have position (0, 0). Added mark rounding data to single player race.xml --- .../src/main/java/mock/app/Event.java | 2 - .../src/main/java/mock/model/MockRace.java | 2 +- .../java/shared/dataInput/RaceXMLReader.java | 2 +- .../src/main/java/shared/model/Boat.java | 4 ++ .../src/main/java/shared/model/Race.java | 8 +-- .../Controllers/HostController.java | 2 +- .../visualiser/model/ResizableRaceCanvas.java | 50 +++++++++---------- .../mock/mockXML/raceSinglePlayer.xml | 12 ++--- 8 files changed, 37 insertions(+), 45 deletions(-) diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index f0783fc0..6246d699 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -72,8 +72,6 @@ public class Event { */ public Event(boolean singlePlayer) throws EventConstructionException { - singlePlayer = false;//TEMP - String raceXMLFile = "mock/mockXML/raceTest.xml"; String boatsXMLFile = "mock/mockXML/boatTest.xml"; String regattaXMLFile = "mock/mockXML/regattaTest.xml"; diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 84e33daf..3970f5d0 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -341,7 +341,7 @@ public class MockRace extends Race { this.updateEstimatedTime(boat); } - checkPosition(boat, totalElapsedMilliseconds); + } private void newOptimalVMG(MockBoat boat) { diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java index 135cd988..02e1afc6 100644 --- a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java +++ b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java @@ -363,7 +363,7 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource { CompoundMark currentCompoundMark = this.compoundMarkMap.get(cornerID); //Sets the rounding type of this compound mark - currentCompoundMark.setRoundingType(RoundingType.valueOf(cornerRounding)); + currentCompoundMark.setRoundingType(RoundingType.getValueOf(cornerRounding)); //Create a leg from these two adjacent compound marks. Leg leg = new Leg(legName, lastCompoundMark, currentCompoundMark, i - 1); diff --git a/racevisionGame/src/main/java/shared/model/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java index d6e28783..385c4f3b 100644 --- a/racevisionGame/src/main/java/shared/model/Boat.java +++ b/racevisionGame/src/main/java/shared/model/Boat.java @@ -82,6 +82,7 @@ public class Boat { /** * The time at which the boat is estimated to reach the next mark, in milliseconds since unix epoch. */ + @Nullable private ZonedDateTime estimatedTimeAtNextMark; /** @@ -106,6 +107,8 @@ public class Boat { this.bearing = Bearing.fromDegrees(0d); + setCurrentPosition(new GPSCoordinate(0, 0)); + this.status = BoatStatusEnum.UNDEFINED; } @@ -365,6 +368,7 @@ public class Boat { * Returns the time at which the boat should reach the next mark. * @return Time at which the boat should reach next mark. */ + @Nullable public ZonedDateTime getEstimatedTimeAtNextMark() { return estimatedTimeAtNextMark; } diff --git a/racevisionGame/src/main/java/shared/model/Race.java b/racevisionGame/src/main/java/shared/model/Race.java index 1a366bfb..b8902d84 100644 --- a/racevisionGame/src/main/java/shared/model/Race.java +++ b/racevisionGame/src/main/java/shared/model/Race.java @@ -342,13 +342,7 @@ public abstract class Race { return lastFps; } - /** - * Returns the legs of this race - * @return list of legs - */ - public List getLegs() { - return legs; - } + /** * Increments the FPS counter, and adds timePeriod milliseconds to our FPS reset timer. diff --git a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java index 3360e514..5966c643 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java @@ -47,7 +47,7 @@ public class HostController extends Controller { */ public void hostGamePressed() throws IOException{ try { - Event game = new Event(true); + Event game = new Event(false); game.start(); connectSocket("localhost", 4942); } catch (EventConstructionException e) { diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index b141eb63..d06ef6f4 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -317,39 +317,35 @@ public class ResizableRaceCanvas extends ResizableCanvas { */ private void drawBoat(VisualiserBoat boat) { - //The position may be null if we haven't received any BoatLocation messages yet. - if (boat.getCurrentPosition() != null) { - - if (boat.isClientBoat()) { - drawClientBoat(boat); - } + if (boat.isClientBoat()) { + drawClientBoat(boat); + } - //Convert position to graph coordinate. - GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition()); + //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() - 6, - pos.getX(), - pos.getX() + 6 }; + //The x coordinates of each vertex of the boat. + double[] x = { + pos.getX() - 6, + pos.getX(), + pos.getX() + 6 }; - //The y coordinates of each vertex of the boat. - double[] y = { - pos.getY() + 12, - pos.getY() - 12, - pos.getY() + 12 }; + //The y coordinates of each vertex of the boat. + double[] y = { + pos.getY() + 12, + pos.getY() - 12, + pos.getY() + 12 }; - //The above shape is essentially a triangle 12px wide, and 24px long. + //The above shape is essentially a triangle 12px wide, and 24px long. - //Draw the boat. - gc.setFill(boat.getColor()); + //Draw the boat. + gc.setFill(boat.getColor()); - gc.save(); - rotate(boat.getBearing().degrees(), pos.getX(), pos.getY()); - gc.fillPolygon(x, y, 3); - gc.restore(); + gc.save(); + rotate(boat.getBearing().degrees(), pos.getX(), pos.getY()); + gc.fillPolygon(x, y, 3); + gc.restore(); - } } @@ -521,7 +517,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { * draws a transparent line around the course that shows the paths boats must travel */ public void drawRaceLine(){ - List legs = this.visualiserRace.getLegs(); + List legs = this.visualiserRace.getVisualiserRaceState().getLegs(); GPSCoordinate legStartPoint = legs.get(0).getStartCompoundMark().getAverageGPSCoordinate(); GPSCoordinate nextStartPoint; for (int i = 0; i < legs.size() -1; i++) { diff --git a/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml b/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml index a5d6761f..553c2571 100644 --- a/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml +++ b/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml @@ -8,12 +8,12 @@ - - - - - - + + + + + + From dc4610d6ebb355826d1e5899de414c9a5f34d52d Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 15 Aug 2017 18:41:28 +1200 Subject: [PATCH 11/16] Fixed sparkline not updating. #story[1095] --- .../src/main/java/shared/model/RaceState.java | 19 ++- .../visualiser/model/ResizableRaceCanvas.java | 1 + .../main/java/visualiser/model/Sparkline.java | 138 +++++++++++++----- .../visualiser/model/VisualiserRaceState.java | 11 +- 4 files changed, 123 insertions(+), 46 deletions(-) diff --git a/racevisionGame/src/main/java/shared/model/RaceState.java b/racevisionGame/src/main/java/shared/model/RaceState.java index d77a4129..19a1a52c 100644 --- a/racevisionGame/src/main/java/shared/model/RaceState.java +++ b/racevisionGame/src/main/java/shared/model/RaceState.java @@ -2,6 +2,8 @@ package shared.model; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import network.Messages.Enums.RaceStatusEnum; import network.Messages.Enums.RaceTypeEnum; import shared.dataInput.BoatDataSource; @@ -37,6 +39,12 @@ public abstract class RaceState { */ private RegattaDataSource regattaDataSource; + /** + * Legs in the race. + * We have this in a separate list so that it can be observed. + */ + private ObservableList legs; + /** @@ -65,6 +73,9 @@ public abstract class RaceState { */ public RaceState() { + //Legs. + this.legs = FXCollections.observableArrayList(); + //Race clock. this.raceClock = new RaceClock(ZonedDateTime.now()); @@ -90,8 +101,9 @@ public abstract class RaceState { * @param legs The new list of legs to use. */ protected void useLegsList(List legs) { + this.legs.setAll(legs); //We add a "dummy" leg at the end of the race. - if (legs.size() > 0) { + if (getLegs().size() > 0) { getLegs().add(new Leg("Finish", getLegs().size())); } } @@ -126,6 +138,7 @@ public abstract class RaceState { public void setRaceDataSource(RaceDataSource raceDataSource) { this.raceDataSource = raceDataSource; this.getRaceClock().setStartingTime(raceDataSource.getStartDateTime()); + useLegsList(raceDataSource.getLegs()); } /** @@ -309,8 +322,8 @@ public abstract class RaceState { * Returns the legs of the race. * @return Legs of the race. */ - public List getLegs() { - return raceDataSource.getLegs(); + public ObservableList getLegs() { + return legs; } diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java index d06ef6f4..d58e2afd 100644 --- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java +++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java @@ -531,6 +531,7 @@ public class ResizableRaceCanvas extends ResizableCanvas { * draws the line leg by leg * @param legs the legs of a race * @param index the index of the current leg to use + * @param legStartPoint The position the current leg. * @return the end point of the current leg that has been drawn */ private GPSCoordinate drawLineRounding(List legs, int index, GPSCoordinate legStartPoint){ diff --git a/racevisionGame/src/main/java/visualiser/model/Sparkline.java b/racevisionGame/src/main/java/visualiser/model/Sparkline.java index d8f21f77..3e49c5a3 100644 --- a/racevisionGame/src/main/java/visualiser/model/Sparkline.java +++ b/racevisionGame/src/main/java/visualiser/model/Sparkline.java @@ -1,14 +1,18 @@ package visualiser.model; import javafx.application.Platform; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.paint.Color; +import shared.model.Leg; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** @@ -32,10 +36,10 @@ public class Sparkline { private ObservableList boats; /** - * The number of legs in the race. - * Used to correctly scale the linechart. + * Race legs to observe. + * We need to observe legs as they may be added after the sparkline is created if race.xml is received after this is created. */ - private Integer legNum; + private ObservableList legs; /** @@ -53,6 +57,14 @@ public class Sparkline { */ private NumberAxis yAxis; + /** + * A map between a boat and its data series in the sparkline. + * This is used so that we can remove a series when (or if) a boat is removed from the race. + */ + private Map> boatSeriesMap; + + + /** * Constructor to set up initial sparkline (LineChart) object @@ -62,12 +74,14 @@ public class Sparkline { public Sparkline(VisualiserRaceState race, LineChart sparklineChart) { this.race = race; this.boats = new SortedList<>(race.getBoats()); - this.legNum = race.getLegCount(); + this.legs = race.getLegs(); this.sparklineChart = sparklineChart; this.yAxis = (NumberAxis) sparklineChart.getYAxis(); this.xAxis = (NumberAxis) sparklineChart.getXAxis(); + this.boatSeriesMap = new HashMap<>(); + createSparkline(); } @@ -79,50 +93,45 @@ public class Sparkline { * Position numbers are displayed. */ private void createSparkline() { - // NOTE: Y axis is in negatives to display correct positions - - //For each boat... - for (VisualiserBoat boat : this.boats) { - - //Create data series for each boat. - XYChart.Series series = new XYChart.Series<>(); - - - //All boats start in "last" place. - series.getData().add(new XYChart.Data<>(0, boats.size())); - //Listen for changes in the boat's leg - we only update the graph when it changes leg. - boat.legProperty().addListener( - (observable, oldValue, newValue) -> { + //We need to dynamically update the sparkline when boats are added/removed. + boats.addListener((ListChangeListener.Change c) -> { - //Get the data to plot. - List boatOrder = race.getLegCompletionOrder().get(oldValue); - //Find boat position in list. - int boatPosition = boatOrder.indexOf(boat) + 1; + Platform.runLater(() -> { - //Get leg number. - int legNumber = oldValue.getLegNumber() + 1; + while (c.next()) { + if (c.wasAdded()) { + for (VisualiserBoat boat : c.getAddedSubList()) { + addBoatSeries(boat); + } - //Create new data point for boat's position at the new leg. - XYChart.Data dataPoint = new XYChart.Data<>(legNumber, boatPosition); + } else if (c.wasRemoved()) { + for (VisualiserBoat boat : c.getRemoved()) { + removeBoatSeries(boat); + } + } - //Add to series. - Platform.runLater(() -> series.getData().add(dataPoint)); + } + //Update height of y axis. + yAxis.setLowerBound(boats.size()); + }); - }); + }); - //Add to chart. - sparklineChart.getData().add(series); - - //Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions. - series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";"); + legs.addListener((ListChangeListener.Change c) -> { + Platform.runLater(() -> xAxis.setUpperBound(race.getLegCount())); + }); + //Initialise chart for existing boats. + for (VisualiserBoat boat : boats) { + addBoatSeries(boat); } + sparklineChart.setCreateSymbols(false); //Set x axis details @@ -131,7 +140,7 @@ public class Sparkline { xAxis.setTickLabelsVisible(false); xAxis.setMinorTickVisible(false); xAxis.setLowerBound(0); - xAxis.setUpperBound(legNum + 2); + xAxis.setUpperBound(race.getLegCount()); xAxis.setTickUnit(1); //Set y axis details @@ -148,6 +157,65 @@ public class Sparkline { } + + /** + * Removes the data series for a given boat from the sparkline. + * @param boat Boat to remove series for. + */ + private void removeBoatSeries(VisualiserBoat boat) { + sparklineChart.getData().remove(boatSeriesMap.get(boat)); + boatSeriesMap.remove(boat); + } + + + /** + * Creates a data series for a boat, and adds it to the sparkline. + * @param boat Boat to add series for. + */ + private void addBoatSeries(VisualiserBoat boat) { + + //Create data series for boat. + XYChart.Series series = new XYChart.Series<>(); + + + //All boats start in "last" place. + series.getData().add(new XYChart.Data<>(0, boats.size())); + + //Listen for changes in the boat's leg - we only update the graph when it changes leg. + boat.legProperty().addListener( + (observable, oldValue, newValue) -> { + + //Get the data to plot. + List boatOrder = race.getLegCompletionOrder().get(oldValue); + //Find boat position in list. + int boatPosition = boatOrder.indexOf(boat) + 1; + + //Get leg number. + int legNumber = oldValue.getLegNumber() + 1; + + + //Create new data point for boat's position at the new leg. + XYChart.Data dataPoint = new XYChart.Data<>(legNumber, boatPosition); + + //Add to series. + Platform.runLater(() -> series.getData().add(dataPoint)); + + + }); + + + //Add to chart. + sparklineChart.getData().add(series); + + //Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions. + series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";"); + + + boatSeriesMap.put(boat, series); + + } + + /** * Converts a color to a hex string, starting with a {@literal #} symbol. * @param color The color to convert. diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java index faefa467..0398e4e5 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java @@ -97,7 +97,7 @@ public class VisualiserRaceState extends RaceState { this.generateVisualiserBoats(this.boats, getBoatDataSource().getBoats(), raceDataSource.getParticipants(), unassignedColors); } - useLegsList(raceDataSource.getLegs()); + initialiseLegCompletionOrder(); } /** @@ -125,14 +125,9 @@ public class VisualiserRaceState extends RaceState { /** - * See {@link RaceState#useLegsList(List)}. - * Also initialises the {@link #legCompletionOrder} map. - * @param legs The new list of legs to use. + * Initialises the {@link #legCompletionOrder} map. */ - @Override - public void useLegsList(List legs) { - super.useLegsList(legs); - + public void initialiseLegCompletionOrder() { //Initialise the leg completion order map. for (Leg leg : getLegs()) { this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size())); From 9ab12a9c58c23728c877afc0938d671e59f29c4a Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 15 Aug 2017 18:48:14 +1200 Subject: [PATCH 12/16] MockBoat: Removed redundant isAutoVMG() function. TackGybeCommand now disables autoVMG. Removed print statements from VMGCommand. --- racevisionGame/src/main/java/mock/model/MockBoat.java | 3 --- racevisionGame/src/main/java/mock/model/MockRace.java | 2 +- .../main/java/mock/model/commandFactory/TackGybeCommand.java | 3 +++ .../src/main/java/mock/model/commandFactory/VMGCommand.java | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/racevisionGame/src/main/java/mock/model/MockBoat.java b/racevisionGame/src/main/java/mock/model/MockBoat.java index c62779d6..f15ec2f2 100644 --- a/racevisionGame/src/main/java/mock/model/MockBoat.java +++ b/racevisionGame/src/main/java/mock/model/MockBoat.java @@ -297,9 +297,6 @@ public class MockBoat extends Boat { this.roundingStatus = 0; } - public boolean isAutoVMG() { - return autoVMG; - } public void setAutoVMG(boolean autoVMG) { this.autoVMG = autoVMG; diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 3970f5d0..3879e3fc 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -335,7 +335,7 @@ public class MockRace extends Race { boat.moveForwards(distanceTravelledMeters); boat.setTimeSinceTackChange(boat.getTimeSinceTackChange() + updatePeriodMilliseconds); - if (boat.isAutoVMG()) { + if (boat.getAutoVMG()) { newOptimalVMG(boat); } diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java index 50023719..d0b0584b 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java @@ -23,6 +23,9 @@ public class TackGybeCommand implements Command { @Override public void execute() { + + boat.setAutoVMG(false); + double boatAngle = boat.getBearing().degrees(); double windAngle =race.getWindDirection().degrees(); double differenceAngle = calcDistance(boatAngle, windAngle); diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java index 8c7d2043..39469cf8 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java @@ -24,10 +24,8 @@ public class VMGCommand implements Command { public void execute() { if (boat.getAutoVMG()){ boat.setAutoVMG(false); - System.out.println("Auto VMG off!"); } else { boat.setAutoVMG(true); - System.out.println("Auto VMG on!"); } } } From 0483859c404fdabdc3cc1b4e09bf34e237352eca Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 15 Aug 2017 20:40:29 +1200 Subject: [PATCH 13/16] Fixed sparkline y axis labelling. Reverted it to how it previously worked (negative data values instead of negative scale). issue #32 --- .../main/java/visualiser/model/Sparkline.java | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/racevisionGame/src/main/java/visualiser/model/Sparkline.java b/racevisionGame/src/main/java/visualiser/model/Sparkline.java index 3e49c5a3..3f7e07b8 100644 --- a/racevisionGame/src/main/java/visualiser/model/Sparkline.java +++ b/racevisionGame/src/main/java/visualiser/model/Sparkline.java @@ -71,7 +71,7 @@ public class Sparkline { * @param race The race to listen to. * @param sparklineChart JavaFX LineChart for the sparkline. */ - public Sparkline(VisualiserRaceState race, LineChart sparklineChart) { + public Sparkline(VisualiserRaceState race, LineChart sparklineChart) { this.race = race; this.boats = new SortedList<>(race.getBoats()); this.legs = race.getLegs(); @@ -115,7 +115,7 @@ public class Sparkline { } //Update height of y axis. - yAxis.setLowerBound(boats.size()); + setYAxisLowerBound(); }); }); @@ -143,17 +143,47 @@ public class Sparkline { xAxis.setUpperBound(race.getLegCount()); xAxis.setTickUnit(1); + + //The y-axis uses negative values, with the minus sign hidden (e.g., boat in 1st has position -1, which becomes 1, boat in 6th has position -6, which becomes 6). + //This is necessary to actually get the y-axis labelled correctly. Negative tick count doesn't work. //Set y axis details - yAxis.setLowerBound(boats.size()); - yAxis.setUpperBound(1); yAxis.setAutoRanging(false); + + yAxis.setTickUnit(1); + yAxis.setMinorTickCount(0); + + yAxis.setUpperBound(0); + setYAxisLowerBound(); + yAxis.setLabel("Position in Race"); - yAxis.setTickUnit(-1);//Negative tick reverses the y axis. yAxis.setTickMarkVisible(true); yAxis.setTickLabelsVisible(true); - yAxis.setTickMarkVisible(true); - yAxis.setMinorTickVisible(true); + yAxis.setMinorTickVisible(false); + + + + //Hide minus number from displaying on axis. + yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) { + @Override + public String toString(Number value) { + if ((value.intValue() == 0) || (value.intValue() < -boats.size())) { + return ""; + + } else { + return String.format("%d", -value.intValue()); + } + } + }); + + } + + + /** + * Sets the lower bound of the y-axis. + */ + private void setYAxisLowerBound() { + yAxis.setLowerBound( -(boats.size() + 1)); } @@ -179,7 +209,7 @@ public class Sparkline { //All boats start in "last" place. - series.getData().add(new XYChart.Data<>(0, boats.size())); + series.getData().add(new XYChart.Data<>(0, -(boats.size()))); //Listen for changes in the boat's leg - we only update the graph when it changes leg. boat.legProperty().addListener( @@ -188,7 +218,7 @@ public class Sparkline { //Get the data to plot. List boatOrder = race.getLegCompletionOrder().get(oldValue); //Find boat position in list. - int boatPosition = boatOrder.indexOf(boat) + 1; + int boatPosition = -(boatOrder.indexOf(boat) + 1); //Get leg number. int legNumber = oldValue.getLegNumber() + 1; From a4755ed88b59afc408a5eaba531991635ff8d5b8 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 15 Aug 2017 21:08:56 +1200 Subject: [PATCH 14/16] Maybe fixed updating boat list on FX thread. --- .../src/main/java/visualiser/model/VisualiserRaceState.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java index 0398e4e5..12ddf37d 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java @@ -1,6 +1,7 @@ package visualiser.model; +import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.paint.Color; @@ -182,7 +183,7 @@ public class VisualiserRaceState extends RaceState { boat.setCurrentLeg(getLegs().get(0)); - existingBoats.add(boat); + Platform.runLater(() -> existingBoats.add(boat)); } From bcb6b79f8d8504ddeafde622dbe07021e3aaa5a2 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 15 Aug 2017 22:41:43 +1200 Subject: [PATCH 15/16] Fixed player boat not being highlighted. #story[1095] --- racevisionGame/src/main/java/mock/app/Event.java | 1 - .../src/main/java/network/Messages/AssignPlayerBoat.java | 1 - .../Commands/VisualiserRaceCommands/RaceStatusCommand.java | 2 +- .../main/java/visualiser/model/VisualiserRaceState.java | 7 +++++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 6246d699..0d95e1c1 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -5,7 +5,6 @@ import mock.exceptions.EventConstructionException; import mock.model.*; import mock.model.commandFactory.CompositeCommand; import network.Messages.LatestMessages; -import network.Messages.RaceSnapshot; import shared.dataInput.*; import shared.enums.XMLFileType; import shared.exceptions.InvalidBoatDataException; diff --git a/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java b/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java index c4197fe9..ab33ac1b 100644 --- a/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java +++ b/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java @@ -1,6 +1,5 @@ package network.Messages; -import network.Messages.Enums.JoinAcceptanceEnum; import network.Messages.Enums.MessageType; diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java index 11fd8c3b..825cd274 100644 --- a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java @@ -103,7 +103,7 @@ public class RaceStatusCommand implements Command { } catch (BoatNotFoundException e) { - Logger.getGlobal().log(Level.WARNING, "RaceStatusCommand.updateBoatStatus: " + this + " could not execute. Boat with sourceID: " + boatStatus.getSourceID() + " not found.", e); + //Logger.getGlobal().log(Level.WARNING, "RaceStatusCommand.updateBoatStatus: " + this + " could not execute. Boat with sourceID: " + boatStatus.getSourceID() + " not found.", e); return; } } diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java index 12ddf37d..588c0816 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java @@ -183,7 +183,10 @@ public class VisualiserRaceState extends RaceState { boat.setCurrentLeg(getLegs().get(0)); - Platform.runLater(() -> existingBoats.add(boat)); + Platform.runLater(() -> { + existingBoats.add(boat); + setPlayerBoat(); + }); } @@ -202,7 +205,7 @@ public class VisualiserRaceState extends RaceState { if (getPlayerBoatID() != 0) { - for (VisualiserBoat boat : getBoats()) { + for (VisualiserBoat boat : new ArrayList<>(getBoats())) { if (boat.getSourceID() == getPlayerBoatID()) { boat.setClientBoat(true); From 85e703cba54200b77391bed79f7d72c8b8185ddd Mon Sep 17 00:00:00 2001 From: fjc40 Date: Tue, 15 Aug 2017 23:10:32 +1200 Subject: [PATCH 16/16] RaceController/info table now makes a copy of the boat list, to avoid race conditions. #story[1095] --- .../main/java/visualiser/Controllers/RaceController.java | 8 +++++++- .../main/java/visualiser/model/VisualiserRaceState.java | 5 +---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index be5b5cd1..3e4c1398 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -208,9 +208,15 @@ public class RaceController extends Controller { public void initialiseInfoTable(VisualiserRaceEvent race) { //Copy list of boats. - SortedList sortedBoats = new SortedList<>(race.getVisualiserRaceState().getBoats()); + ObservableList boats = FXCollections.observableArrayList(race.getVisualiserRaceState().getBoats()); + SortedList sortedBoats = new SortedList<>(boats); sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty()); + //Update copy when original changes. + race.getVisualiserRaceState().getBoats().addListener((ListChangeListener.Change c) -> Platform.runLater(() -> { + boats.setAll(race.getVisualiserRaceState().getBoats()); + })); + //Set up table. boatInfoTable.setItems(sortedBoats); diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java index 588c0816..b1767cd5 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java @@ -183,10 +183,7 @@ public class VisualiserRaceState extends RaceState { boat.setCurrentLeg(getLegs().get(0)); - Platform.runLater(() -> { - existingBoats.add(boat); - setPlayerBoat(); - }); + existingBoats.add(boat); }