Added ClientConnection and server-side handshake. Added MessageSerialiser and Deserialiser. #story[1095]main
parent
3ec87582d3
commit
7cc39abe57
@ -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<Byte, ConnectionStateEnum> 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<AC35Data> outputQueue;
|
||||
|
||||
/**
|
||||
* Used to read messages from socket.
|
||||
*/
|
||||
private MessageDeserialiser messageDeserialiser;
|
||||
|
||||
/**
|
||||
* Stores messages read from socket.
|
||||
*/
|
||||
private BlockingQueue<AC35Data> 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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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<AC35Data> 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<AC35Data> 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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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<Integer> unallocatedIDs = new ArrayList<>();
|
||||
|
||||
|
||||
/**
|
||||
* This list contains all allocated source IDs.
|
||||
*/
|
||||
List<Integer> 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<Integer> 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);
|
||||
}
|
||||
}
|
||||
@ -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<Command> 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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package network.MessageControllers;
|
||||
|
||||
|
||||
|
||||
public class MessageController {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package network.MessageRouters;
|
||||
|
||||
|
||||
/**
|
||||
* This class routes {@link network.Messages.AC35Data} messages to an appropriate message controller.
|
||||
*/
|
||||
public class MessageRouter {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -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<AC35Data> snapshot;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a snapshot using a given list of messages.
|
||||
* @param snapshot Messages to use as snapshot.
|
||||
*/
|
||||
public RaceSnapshot(List<AC35Data> snapshot) {
|
||||
this.snapshot = snapshot;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the contents of the snapshot.
|
||||
* This is a shallow copy.
|
||||
* @return Contents of the snapshot.
|
||||
*/
|
||||
public List<AC35Data> getSnapshot() {
|
||||
|
||||
List<AC35Data> copy = new ArrayList<>(snapshot);
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@ -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<AC35Data> 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<AC35Data> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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<AC35Data> 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<AC35Data> 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<AC35Data> 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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<AC35Data> outputQueue;
|
||||
|
||||
/**
|
||||
* Used to read messages from socket.
|
||||
*/
|
||||
private MessageDeserialiser messageDeserialiser;
|
||||
|
||||
/**
|
||||
* Stores messages read from socket.
|
||||
*/
|
||||
private BlockingQueue<AC35Data> 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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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<Integer> 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<Integer> 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();
|
||||
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue