diff --git a/.mailmap b/.mailmap index cad3a3a8..caf8a624 100644 --- a/.mailmap +++ b/.mailmap @@ -23,5 +23,5 @@ Haoming Yin Peter Galloway Peter Zhi You Tan zyt10 Zhi You Tan Ryan Tan -Alistair McIntyre alistairjmcintyre +Alistair McIntyre Calum cir27 \ No newline at end of file diff --git a/pom.xml b/pom.xml index aac3b0c7..fca06536 100644 --- a/pom.xml +++ b/pom.xml @@ -69,8 +69,8 @@ commons-cli 1.4 - + diff --git a/src/main/java/seng302/App.java b/src/main/java/seng302/App.java index 3b914350..8c6f85ac 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -67,8 +67,6 @@ public class App extends Application { @Override public void start(Stage primaryStage) throws Exception { - PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv")); - Parent root = FXMLLoader.load(getClass().getResource("/views/StartScreenView.fxml")); primaryStage.setTitle("RaceVision"); Scene scene = new Scene(root, 1530, 960); diff --git a/src/main/java/seng302/gameServer/GameState.java b/src/main/java/seng302/gameServer/GameState.java index 4c7ec9eb..8d2e2f6e 100644 --- a/src/main/java/seng302/gameServer/GameState.java +++ b/src/main/java/seng302/gameServer/GameState.java @@ -1,17 +1,17 @@ package seng302.gameServer; + +import java.util.*; +import seng302.gameServer.server.messages.BoatAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; - +import seng302.model.Player; +import seng302.model.Yacht; +import seng302.model.mark.MarkOrder; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import seng302.model.GeoPoint; -import seng302.gameServer.server.messages.BoatActionType; -import seng302.model.Player; -import seng302.model.Yacht; -import seng302.model.mark.MarkOrder; import seng302.model.mark.CompoundMark; import seng302.model.mark.Mark; import seng302.utilities.GeoUtility; @@ -25,6 +25,7 @@ public class GameState implements Runnable { private Logger logger = LoggerFactory.getLogger(MarkOrder.class); private static Integer STATE_UPDATES_PER_SECOND = 60; + public static Integer MAX_PLAYERS = 8; private static Long previousUpdateTime; public static Double windDirection; @@ -61,10 +62,8 @@ public class GameState implements Runnable { players = new ArrayList<>(); currentStage = GameStages.LOBBYING; isRaceStarted = false; - yachts = new HashMap<>(); //set this when game stage changes to prerace previousUpdateTime = System.currentTimeMillis(); - yachts = new HashMap<>(); markOrder = new MarkOrder(); //This could be instantiated at some point with a select map? new Thread(this).start(); //Run the auto updates on the game state @@ -86,7 +85,8 @@ public class GameState implements Runnable { public static void addPlayer(Player player) { players.add(player); - String playerText = player.getYacht().getSourceId() + " " + player.getYacht().getBoatName() + " " + player.getYacht().getCountry(); + String playerText = player.getYacht().getSourceId() + " " + player.getYacht().getBoatName() + + " " + player.getYacht().getCountry(); playerStringMap.put(player, playerText); } @@ -143,7 +143,7 @@ public class GameState implements Runnable { return yachts; } - public static void updateBoat(Integer sourceId, BoatActionType actionType) { + public static void updateBoat(Integer sourceId, BoatAction actionType) { Yacht playerYacht = yachts.get(sourceId); // System.out.println("-----------------------"); switch (actionType) { diff --git a/src/main/java/seng302/gameServer/HeartbeatThread.java b/src/main/java/seng302/gameServer/HeartbeatThread.java index f15868c3..b168a197 100644 --- a/src/main/java/seng302/gameServer/HeartbeatThread.java +++ b/src/main/java/seng302/gameServer/HeartbeatThread.java @@ -42,7 +42,6 @@ public class HeartbeatThread extends Thread{ */ private void sendHeartbeatToAllPlayers(){ Message heartbeat = new Heartbeat(seqNum); - for (Player player : GameState.getPlayers()){ if (!player.getSocket().isConnected()) { playerLostConnection(player); @@ -54,7 +53,6 @@ public class HeartbeatThread extends Thread{ playerLostConnection(player); } } - updateDelegate(); seqNum++; } @@ -71,7 +69,6 @@ public class HeartbeatThread extends Thread{ public void run(){ Timer t = new Timer(); - t.schedule(new TimerTask() { @Override public void run() { diff --git a/src/main/java/seng302/gameServer/MainServerThread.java b/src/main/java/seng302/gameServer/MainServerThread.java index cd762501..74a70dd1 100644 --- a/src/main/java/seng302/gameServer/MainServerThread.java +++ b/src/main/java/seng302/gameServer/MainServerThread.java @@ -15,6 +15,7 @@ import seng302.model.Yacht; import seng302.model.mark.CompoundMark; import seng302.utilities.GeoUtility; import seng302.visualiser.GameClient; +import seng302.model.PolarTable; /** * A class describing the overall server, which creates and collects server threads for each client @@ -36,12 +37,13 @@ public class MainServerThread extends Observable implements Runnable, ClientConn private GameClient gameClient; public MainServerThread() { + new GameState("localhost"); try { serverSocket = new ServerSocket(PORT); } catch (IOException e) { serverLog("IO error in server thread handler upon trying to make new server socket", 0); } - + PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv")); terminated = false; thread = new Thread(this); thread.start(); @@ -113,9 +115,11 @@ public class MainServerThread extends Observable implements Runnable, ClientConn public void clientConnected(ServerToClientThread serverToClientThread) { serverLog("Player Connected From " + serverToClientThread.getThread().getName(), 0); serverToClientThreads.add(serverToClientThread); - this.addObserver(serverToClientThread); - setChanged(); - notifyObservers("send setup message"); + serverToClientThread.addConnectionListener(() -> { + for (ServerToClientThread thread : serverToClientThreads) { + thread.sendSetupMessages(); + } + }); } /** @@ -133,11 +137,15 @@ public class MainServerThread extends Observable implements Runnable, ClientConn serverLog("Player " + player.getYacht().getSourceId() + "'s socket disconnected", 0); GameState.removeYacht(player.getYacht().getSourceId()); GameState.removePlayer(player); + ServerToClientThread closedConnection = null; for (ServerToClientThread serverToClientThread : serverToClientThreads) { if (serverToClientThread.getSocket() == player.getSocket()) { - this.deleteObserver(serverToClientThread); + closedConnection = serverToClientThread; + } else { + serverToClientThread.sendSetupMessages(); } } + serverToClientThreads.remove(closedConnection); setChanged(); notifyObservers("send setup message"); } diff --git a/src/main/java/seng302/gameServer/ServerPacketParser.java b/src/main/java/seng302/gameServer/ServerPacketParser.java index 0d631eb1..84f8fd17 100644 --- a/src/main/java/seng302/gameServer/ServerPacketParser.java +++ b/src/main/java/seng302/gameServer/ServerPacketParser.java @@ -1,37 +1,26 @@ package seng302.gameServer; import java.util.Arrays; + +import seng302.gameServer.server.messages.ClientType; +import seng302.gameServer.server.messages.Message; import seng302.model.stream.packets.StreamPacket; -import seng302.gameServer.server.messages.BoatActionType; +import seng302.gameServer.server.messages.BoatAction; public class ServerPacketParser { - - public static BoatActionType extractBoatAction(StreamPacket packet) { + public static BoatAction extractBoatAction(StreamPacket packet) { byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; - long actionTypeValue = bytesToLong(Arrays.copyOfRange(payload, 0, 1)); - return BoatActionType.getType((int) actionTypeValue); + long actionTypeValue = Message.bytesToLong(Arrays.copyOfRange(payload, 0, 1)); + return BoatAction.getType((int) actionTypeValue); } - /** - * takes an array of up to 7 bytes and returns a positive - * long constructed from the input bytes - * - * @return a positive long if there is less than 7 bytes -1 otherwise - */ - private static long bytesToLong(byte[] bytes) { - long partialLong = 0; - int index = 0; - for (byte b : bytes) { - if (index > 6) { - return -1; - } - partialLong = partialLong | (b & 0xFFL) << (index * 8); - index++; - } - return partialLong; + public static ClientType extractClientType(StreamPacket packet){ + byte[] payload = packet.getPayload(); + long value = Message.bytesToLong(Arrays.copyOfRange(payload, 0, 1)); + return ClientType.getClientType((int) value); } } diff --git a/src/main/java/seng302/gameServer/ServerToClientThread.java b/src/main/java/seng302/gameServer/ServerToClientThread.java index 3fdc3514..767ebc77 100644 --- a/src/main/java/seng302/gameServer/ServerToClientThread.java +++ b/src/main/java/seng302/gameServer/ServerToClientThread.java @@ -12,8 +12,6 @@ import java.net.SocketException; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import java.util.Observable; -import java.util.Observer; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import java.util.zip.CRC32; @@ -26,23 +24,41 @@ import seng302.model.stream.packets.StreamPacket; import seng302.model.stream.xml.generator.Race; import seng302.model.stream.xml.generator.Regatta; import seng302.utilities.XMLGenerator; -import seng302.gameServer.server.messages.BoatActionType; +import seng302.gameServer.server.messages.BoatAction; import seng302.gameServer.server.messages.BoatLocationMessage; import seng302.gameServer.server.messages.BoatStatus; import seng302.gameServer.server.messages.BoatSubMessage; +import seng302.gameServer.server.messages.ClientType; import seng302.gameServer.server.messages.Message; import seng302.gameServer.server.messages.RaceStatus; import seng302.gameServer.server.messages.RaceStatusMessage; import seng302.gameServer.server.messages.RaceType; +import seng302.gameServer.server.messages.RegistrationResponseMessage; +import seng302.gameServer.server.messages.RegistrationResponseStatus; import seng302.gameServer.server.messages.XMLMessage; import seng302.gameServer.server.messages.XMLMessageSubType; +import seng302.model.Player; +import seng302.model.Yacht; +import seng302.model.stream.packets.PacketType; +import seng302.model.stream.packets.StreamPacket; +import seng302.model.stream.xml.generator.Race; +import seng302.model.stream.xml.generator.Regatta; +import seng302.utilities.XMLGenerator; /** * A class describing a single connection to a Client for the purposes of sending and receiving on * its own thread. All server threads created and owned by the server thread handler which can * trigger client updates on its threads Created by wmu16 on 13/07/17. */ -public class ServerToClientThread implements Runnable, Observer { +public class ServerToClientThread implements Runnable { + + /** + * Called to notify listeners when this thread receives a connection correctly. + */ + @FunctionalInterface + interface ConnectionListener { + void notifyConnection (); + } private static final Integer LOG_LEVEL = 1; private static final Integer MAX_ID_ATTEMPTS = 10; @@ -55,41 +71,55 @@ public class ServerToClientThread implements Runnable, Observer { private ByteArrayOutputStream crcBuffer; - private Boolean userIdentified = false; - private Boolean connected = true; - private Boolean updateClient = true; // private Boolean initialisedRace = true; private Integer seqNo; private Integer sourceId; + private ClientType clientType; + private Boolean isRegistered = false; + private XMLGenerator xml; + private List connectionListeners = new ArrayList<>(); + private Yacht yacht; public ServerToClientThread(Socket socket) { this.socket = socket; + seqNo = 0; + + try{ + is = socket.getInputStream(); + os = socket.getOutputStream(); + } catch (IOException e) { + return; + } + + thread = new Thread(this); + thread.start(); + } + + private void setUpPlayer(){ BufferedReader fn; String fName = ""; BufferedReader ln; String lName = ""; - try { - is = socket.getInputStream(); - os = socket.getOutputStream(); - fn = new BufferedReader( + + fn = new BufferedReader( new InputStreamReader( - ServerToClientThread.class.getResourceAsStream( - "/server_config/CSV_Database_of_First_Names.csv" - ) + ServerToClientThread.class.getResourceAsStream( + "/server_config/CSV_Database_of_First_Names.csv" + ) ) - ); - List all = fn.lines().collect(Collectors.toList()); - fName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); - ln = new BufferedReader( + ); + List all = fn.lines().collect(Collectors.toList()); + fName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); + ln = new BufferedReader( new InputStreamReader( - ServerToClientThread.class.getResourceAsStream( - "/server_config/CSV_Database_of_Last_Names.csv" - ) + ServerToClientThread.class.getResourceAsStream( + "/server_config/CSV_Database_of_Last_Names.csv" + ) ) ); all = ln.lines().collect(Collectors.toList()); @@ -102,20 +132,16 @@ public class ServerToClientThread implements Runnable, Observer { if (threeWayHandshake(sourceId)) { serverLog("Successful handshake. Client allocated id: " + sourceId, 0); yacht = new Yacht( - "Yacht", sourceId, sourceId.toString(), fName, fName + " " + lName, "NZ" - ); -// Yacht yacht = new Yacht("Kappa", "Kap", new GeoPoint(57.6708220, 11.8321340), 90.0); - GameState.addYacht(sourceId, yacht); - GameState.addPlayer(new Player(socket, yacht)); - } else { - serverLog("Unsuccessful handshake. Connection rejected", 1); - closeSocket(); - return; - } + ); + all = ln.lines().collect(Collectors.toList()); + lName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); - seqNo = 0; - thread = new Thread(this); - thread.start(); + + Yacht yacht = new Yacht( + "Yacht", sourceId, sourceId.toString(), fName, fName + " " + lName, "NZ" + ); + GameState.addYacht(sourceId, yacht); + GameState.addPlayer(new Player(socket, yacht)); } static void serverLog(String message, int logLevel) { @@ -132,6 +158,35 @@ public class ServerToClientThread implements Runnable, Observer { } } + private void completeRegistration(ClientType clientType) throws IOException { + // Fail if not a player + if (!clientType.equals(ClientType.PLAYER)){ + RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(0, RegistrationResponseStatus.FAILURE_GENERAL); + os.write(responseMessage.getBuffer()); + return; + } + + if (GameState.getPlayers().size() >= GameState.MAX_PLAYERS){ + RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(0, RegistrationResponseStatus.FAILURE_FULL); + os.write(responseMessage.getBuffer()); + return; + } + + Integer sourceId = GameState.getUniquePlayerID(); + RegistrationResponseMessage responseMessage = new RegistrationResponseMessage(sourceId, RegistrationResponseStatus.SUCCESS_PLAYING); + + this.clientType = clientType; + this.sourceId = sourceId; + isRegistered = true; + os.write(responseMessage.getBuffer()); + + setUpPlayer(); + + for (ConnectionListener listener : connectionListeners) { + listener.notifyConnection(); + } + } + public void run() { int sync1; int sync2; @@ -140,20 +195,6 @@ public class ServerToClientThread implements Runnable, Observer { while (socket.isConnected()) { try { - //Perform a write if it is time to as delegated by the MainServerThread - if (updateClient) { - // TODO: 13/07/17 wmu16 - Write out game state - some function that would write all appropriate messages to this output stream -// ChatterMessage chatterMessage = new ChatterMessage(4, 14, "Hello, it's me"); -// sendMessage(chatterMessage); -// try { -// GameState.outputState(os); -// } catch (IOException e) { -// System.out.println("IO error in server thread upon writing to output stream"); -// } -// sendBoatLocationPackets(); - updateClient = false; - } - crcBuffer = new ByteArrayOutputStream(); sync1 = readByte(); sync2 = readByte(); @@ -173,11 +214,18 @@ public class ServerToClientThread implements Runnable, Observer { //System.out.println("RECEIVED A PACKET"); switch (PacketType.assignPacketType(type, payload)) { case BOAT_ACTION: - BoatActionType actionType = ServerPacketParser - .extractBoatAction( - new StreamPacket(type, payloadLength, timeStamp, payload)); + BoatAction actionType = ServerPacketParser + .extractBoatAction( + new StreamPacket(type, payloadLength, timeStamp, payload)); GameState.updateBoat(sourceId, actionType); break; + + case RACE_REGISTRATION_REQUEST: + ClientType requestedType = ServerPacketParser.extractClientType( + new StreamPacket(type, payloadLength, timeStamp, payload)); + + completeRegistration(requestedType); + break; } } else { serverLog("Packet has been dropped", 1); @@ -192,7 +240,7 @@ public class ServerToClientThread implements Runnable, Observer { } } - private void sendSetupMessages() { + public void sendSetupMessages() { xml = new XMLGenerator(); Race race = new Race(); @@ -220,40 +268,6 @@ public class ServerToClientThread implements Runnable, Observer { public void updateClient() { sendBoatLocationPackets(); - updateClient = true; - } - - - /** - * Tries to confirm the connection just accepted. - * Sends ID, expects that ID echoed for confirmation, - * if so, sends a confirmation packet back to that connection - * Creates a player instance with that ID and this thread and adds it to the GameState - * If not, close the socket and end the threads execution - * - * @param id the id to try and assign to the connection - * @return A boolean indicating if it was a successful handshake - */ - private Boolean threeWayHandshake(Integer id) { - Integer confirmationID = null; - Integer identificationAttempt = 0; - while (!userIdentified) { - try { - os.write(id); //Send out new ID looking for echo - confirmationID = is.read(); - } catch (IOException e) { - serverLog("Three way handshake failed", 1); - } - - if (id.equals(confirmationID)) { //ID is echoed back. Connection is a client - return true; - } else if (identificationAttempt > MAX_ID_ATTEMPTS) { //No response. not a client. tidy up and go home. - return false; - } - identificationAttempt++; - } - - return true; } private void closeSocket() { @@ -264,7 +278,6 @@ public class ServerToClientThread implements Runnable, Observer { } } - private int readByte() throws Exception { int currentByte = -1; try { @@ -314,7 +327,6 @@ public class ServerToClientThread implements Runnable, Observer { private void sendBoatLocationPackets() { ArrayList yachts = new ArrayList<>(GameState.getYachts().values()); for (Yacht yacht : yachts) { -// System.out.println("[SERVER] Lat: " + yacht.getLocation().getLat() + " Lon: " + yacht.getLocation().getLng()); BoatLocationMessage boatLocationMessage = new BoatLocationMessage( yacht.getSourceId(), @@ -335,7 +347,6 @@ public class ServerToClientThread implements Runnable, Observer { public void sendRaceStatusMessage() { // variables taken from GameServerThread - List boatSubMessages = new ArrayList<>(); BoatStatus boatStatus; RaceStatus raceStatus; @@ -378,4 +389,12 @@ public class ServerToClientThread implements Runnable, Observer { public void sendCollisionMessage(Integer yachtId) { sendMessage(new YachtEventCodeMessage(yachtId)); } + + public void addConnectionListener(ConnectionListener listener) { + connectionListeners.add(listener); + } + + public void removeConnectionListener(ConnectionListener listener) { + connectionListeners.remove(listener); + } } diff --git a/src/main/java/seng302/gameServer/server/messages/BoatActionType.java b/src/main/java/seng302/gameServer/server/messages/BoatAction.java similarity index 62% rename from src/main/java/seng302/gameServer/server/messages/BoatActionType.java rename to src/main/java/seng302/gameServer/server/messages/BoatAction.java index 53fc6018..cc53668c 100644 --- a/src/main/java/seng302/gameServer/server/messages/BoatActionType.java +++ b/src/main/java/seng302/gameServer/server/messages/BoatAction.java @@ -6,29 +6,30 @@ import java.util.Map; /** * Created by kre39 on 12/07/17. */ -public enum BoatActionType { +public enum BoatAction { VMG(1), SAILS_IN(2), SAILS_OUT(3), TACK_GYBE(4), UPWIND(5), - DOWNWIND(6); + DOWNWIND(6), + MAINTAIN_HEADING(7); private final int type; - private static final Map intToTypeMap = new HashMap<>(); + private static final Map intToTypeMap = new HashMap<>(); static { - for (BoatActionType type : BoatActionType.values()) { + for (BoatAction type : BoatAction.values()) { intToTypeMap.put(type.getValue(), type); } } - BoatActionType(int type){ + BoatAction(int type){ this.type = type; } - public static BoatActionType getType(int value) { + public static BoatAction getType(int value) { return intToTypeMap.get(value); } diff --git a/src/main/java/seng302/gameServer/server/messages/BoatActionMessage.java b/src/main/java/seng302/gameServer/server/messages/BoatActionMessage.java index cfa596f7..2fc084e5 100644 --- a/src/main/java/seng302/gameServer/server/messages/BoatActionMessage.java +++ b/src/main/java/seng302/gameServer/server/messages/BoatActionMessage.java @@ -6,9 +6,9 @@ package seng302.gameServer.server.messages; public class BoatActionMessage extends Message{ private final MessageType MESSAGE_TYPE = MessageType.BOAT_ACTION; private final int MESSAGE_SIZE = 1; - private BoatActionType actionType; + private BoatAction actionType; - public BoatActionMessage(BoatActionType actionType) { + public BoatActionMessage(BoatAction actionType) { this.actionType = actionType; setHeader(new Header(MessageType.BOAT_ACTION, 0, (short) 1)); // the second variable is the source id allocateBuffer(); diff --git a/src/main/java/seng302/gameServer/server/messages/ClientType.java b/src/main/java/seng302/gameServer/server/messages/ClientType.java new file mode 100644 index 00000000..b96ca5c7 --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/ClientType.java @@ -0,0 +1,33 @@ +package seng302.gameServer.server.messages; + +public enum ClientType { + SPECTATOR(0x00), + PLAYER(0x01), + CONTROL_TUTORIAL(0x02), + GHOST_MODE(0x03); + + private int type; + + ClientType(int type){ + this.type = type; + } + + public int getCode(){ + return type; + } + + public static ClientType getClientType(int typeCode){ + switch (typeCode){ + case 0x00: + return SPECTATOR; + case 0x01: + return PLAYER; + case 0x02: + return CONTROL_TUTORIAL; + case 0x03: + return GHOST_MODE; + default: + return PLAYER; + } + } +} diff --git a/src/main/java/seng302/gameServer/server/messages/MessageType.java b/src/main/java/seng302/gameServer/server/messages/MessageType.java index 1d25e61d..4552d781 100644 --- a/src/main/java/seng302/gameServer/server/messages/MessageType.java +++ b/src/main/java/seng302/gameServer/server/messages/MessageType.java @@ -17,7 +17,9 @@ public enum MessageType { MARK_ROUNDING(38), COURSE_WIND(44), AVERAGE_WIND(47), - BOAT_ACTION(100); + BOAT_ACTION(100), + REGISTRATION_REQUEST(101), + REGISTRATION_RESPONSE(102); private int code; @@ -32,4 +34,6 @@ public enum MessageType { int getCode(){ return this.code; } + + } diff --git a/src/main/java/seng302/gameServer/server/messages/RegistrationRequestMessage.java b/src/main/java/seng302/gameServer/server/messages/RegistrationRequestMessage.java new file mode 100644 index 00000000..59757a18 --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/RegistrationRequestMessage.java @@ -0,0 +1,22 @@ +package seng302.gameServer.server.messages; + + +public class RegistrationRequestMessage extends Message { + private static int MESSAGE_LENGTH = 2; + + public RegistrationRequestMessage(ClientType type){ + setHeader(new Header(MessageType.REGISTRATION_REQUEST, 1, (short) getSize())); + + allocateBuffer(); + writeHeaderToBuffer(); + + putInt(type.getCode(), 2); + + writeCRC(); + } + + @Override + public int getSize() { + return MESSAGE_LENGTH; + } +} diff --git a/src/main/java/seng302/gameServer/server/messages/RegistrationResponseMessage.java b/src/main/java/seng302/gameServer/server/messages/RegistrationResponseMessage.java new file mode 100644 index 00000000..e2174da4 --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/RegistrationResponseMessage.java @@ -0,0 +1,20 @@ +package seng302.gameServer.server.messages; + +public class RegistrationResponseMessage extends Message{ + + public RegistrationResponseMessage(int clientSourceID, RegistrationResponseStatus status){ + setHeader(new Header(MessageType.REGISTRATION_RESPONSE, 1, (short) getSize())); + allocateBuffer(); + writeHeaderToBuffer(); + + putInt(clientSourceID, 4); + putInt(status.getCode(), 1); + + writeCRC(); + } + + @Override + public int getSize() { + return 5; + } +} diff --git a/src/main/java/seng302/gameServer/server/messages/RegistrationResponseStatus.java b/src/main/java/seng302/gameServer/server/messages/RegistrationResponseStatus.java new file mode 100644 index 00000000..2ae47a92 --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/RegistrationResponseStatus.java @@ -0,0 +1,44 @@ +package seng302.gameServer.server.messages; + +public enum RegistrationResponseStatus { + SUCCESS_SPECTATING(0x00), + SUCCESS_PLAYING(0x01), + SUCCESS_TUTORIAL(0x02), + SUCCESS_GHOSTING(0x03), + + FAILURE_GENERAL(0x10), + FAILURE_FULL(0x11); + + private int code; + + RegistrationResponseStatus(int code){ + this.code = code; + } + + /** + * Get the message code (From the API Spec) + * @return the message code + */ + int getCode(){ + return this.code; + } + + public static RegistrationResponseStatus getResponseStatus(int typeCode){ + switch (typeCode){ + case 0x00: + return SUCCESS_SPECTATING; + case 0x01: + return SUCCESS_PLAYING; + case 0x02: + return SUCCESS_TUTORIAL; + case 0x03: + return SUCCESS_GHOSTING; + case 0x10: + return FAILURE_GENERAL; + case 0x11: + return FAILURE_FULL; + default: + return FAILURE_GENERAL; + } + } +} diff --git a/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java b/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java index e4cbf676..36164af2 100644 --- a/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java +++ b/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java @@ -8,10 +8,10 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import seng302.model.mark.CompoundMark; import seng302.gameServer.server.simulator.Corner; -import seng302.model.mark.Mark; import seng302.gameServer.server.simulator.RoundingType; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Mark; /** * Parses the race xml file to get course details @@ -84,17 +84,17 @@ public class CourseParser extends FileParser { NodeList marks = e.getElementsByTagName("Mark"); List subMarks = new ArrayList<>(); for (int i = 0; i < marks.getLength(); i++) { - Mark mark = getMark(marks.item(i)); + Mark mark = getMark(marks.item(i)); if (mark != null) { subMarks.add(mark); } - } + } return new CompoundMark(markID, name, subMarks); } - System.out.println("Failed to create compound mark."); - return null; - } + System.out.println("Failed to create compound mark."); + return null; + } private Mark getMark(Node node) { diff --git a/src/main/java/seng302/model/PolarTable.java b/src/main/java/seng302/model/PolarTable.java index dee22278..9334cc54 100644 --- a/src/main/java/seng302/model/PolarTable.java +++ b/src/main/java/seng302/model/PolarTable.java @@ -71,8 +71,6 @@ public final class PolarTable { } catch (IOException e) { System.out.println("[PolarTable] IO exception"); } - - } @@ -155,7 +153,6 @@ public final class PolarTable { public static Double getClosestWindSpeedInPolar(Double thisWindSpeed) { Double smallestDif = Double.POSITIVE_INFINITY; Double closestWind = 0d; - for (Double polarWindSpeed : polarTable.keySet()) { Double difference = Math.abs(polarWindSpeed - thisWindSpeed); if (difference < smallestDif) { diff --git a/src/main/java/seng302/model/Yacht.java b/src/main/java/seng302/model/Yacht.java index 4a48a07f..94cb4948 100644 --- a/src/main/java/seng302/model/Yacht.java +++ b/src/main/java/seng302/model/Yacht.java @@ -32,7 +32,7 @@ public class Yacht extends Observable { @FunctionalInterface public interface YachtLocationListener { - void notifyLocation(Yacht yacht, double lat, double lon, double heading, double velocity); + void notifyLocation(Yacht yacht, double lat, double lon, double heading, double velocity, boolean sailIn); } private Logger logger = LoggerFactory.getLogger(Yacht.class); @@ -62,12 +62,14 @@ public class Yacht extends Observable { private Integer legNumber = 0; //SERVER SIDE - private final Double TURN_STEP = 5.0; + public static final Double TURN_STEP = 5.0; //This should be in some utils class somewhere 2bh. Public for tests sake. private Double lastHeading; - private Boolean sailIn; + private Boolean sailIn = false; private GeoPoint location; private Integer boatStatus; private Double velocity; + private Boolean isAuto; + private Double autoHeading; //MARK ROUNDING INFO private GeoPoint lastLocation; //For purposes of mark rounding calculations @@ -85,6 +87,7 @@ public class Yacht extends Observable { private CompoundMark lastMarkRounded; private Integer positionInt = 0; private Color colour; + private Boolean clientSailsIn = true; public Yacht(String boatType, Integer sourceId, String hullID, String shortName, String boatName, String country) { @@ -95,6 +98,7 @@ public class Yacht extends Observable { this.boatName = boatName; this.country = country; this.sailIn = false; + this.isAuto = false; this.location = new GeoPoint(57.670341, 11.826856); this.lastLocation = location; this.heading = 120.0; //In degrees @@ -152,6 +156,11 @@ public class Yacht extends Observable { } } + runAutoPilot(); + + //UPDATE BOAT LOCATION + lastLocation = location; + location = GeoUtility.getGeoCoordinate(location, heading, velocity * secondsElapsed); Double metersCovered = velocity * secondsElapsed; GeoPoint calculatedPoint = getGeoCoordinate(location, heading, metersCovered); @@ -390,15 +399,60 @@ public class Yacht extends Observable { } + /** + * Adjusts the heading of the boat by a given amount, while recording the boats + * last heading. + * + * @param amount the amount by which to adjust the boat heading. + */ public void adjustHeading(Double amount) { Double newVal = heading + amount; lastHeading = heading; heading = (double) Math.floorMod(newVal.longValue(), 360L); } + /** + * Swaps the boats direction from one side of the wind to the other. + */ public void tackGybe(Double windDirection) { - Double normalizedHeading = normalizeHeading(); - adjustHeading(-2 * normalizedHeading); + if (isAuto) { + disableAutoPilot(); + } else { + Double normalizedHeading = normalizeHeading(); + Double newVal = (-2 * normalizedHeading) + heading; + Double newHeading = (double) Math.floorMod(newVal.longValue(), 360L); + setAutoPilot(newHeading); + } + } + + /** + * Enables the boats auto pilot feature, which will move the boat towards a given heading. + * @param thisHeading The heading to move the boat towards. + */ + private void setAutoPilot(Double thisHeading) { + isAuto = true; + autoHeading = thisHeading; + } + + /** + * Disables the auto pilot function. + */ + public void disableAutoPilot() { + isAuto = false; + } + + /** + * Moves the boat towards the given heading when the auto pilot was set. Disables the auto pilot + * in the event that the boat is within the range of 1 turn step of its goal. + */ + public void runAutoPilot() { + if (isAuto) { + turnTowardsHeading(autoHeading); + if (Math.abs(heading - autoHeading) + <= TURN_STEP) { //Cancel when within 1 turn step of target. + isAuto = false; + } + } } public void toggleSailIn() { @@ -406,6 +460,7 @@ public class Yacht extends Observable { } public void turnUpwind() { + disableAutoPilot(); Double normalizedHeading = normalizeHeading(); if (normalizedHeading == 0) { if (lastHeading < 180) { @@ -427,6 +482,7 @@ public class Yacht extends Observable { } public void turnDownwind() { + disableAutoPilot(); Double normalizedHeading = normalizeHeading(); if (normalizedHeading == 0) { if (lastHeading < 180) { @@ -447,38 +503,59 @@ public class Yacht extends Observable { } } + /** + * Takes the VMG from the polartable for upwind or downwind depending on the boats direction, + * and uses this to calculate a heading to move the yacht towards. + */ public void turnToVMG() { - Double normalizedHeading = normalizeHeading(); - Double optimalHeading; - HashMap optimalPolarMap; - - if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind - optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots()); - optimalHeading = optimalPolarMap.keySet().iterator().next(); + if (isAuto) { + disableAutoPilot(); } else { - optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots()); - optimalHeading = optimalPolarMap.keySet().iterator().next(); - } - // Take optimal heading and turn into correct - optimalHeading = - optimalHeading + (double) Math.floorMod(GameState.getWindDirection().longValue(), 360L); + Double normalizedHeading = normalizeHeading(); + Double optimalHeading; + HashMap optimalPolarMap; - turnTowardsHeading(optimalHeading); - - } - - private void turnTowardsHeading(Double newHeading) { - if (heading < 90 && newHeading > 270) { - adjustHeading(-TURN_STEP); - } else { - if (heading < newHeading) { - adjustHeading(TURN_STEP); + if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind + optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots()); } else { - adjustHeading(-TURN_STEP); + optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots()); } + optimalHeading = optimalPolarMap.keySet().iterator().next(); + + if (normalizedHeading > 180) { + optimalHeading = 360 - optimalHeading; + } + + // Take optimal heading and turn into a boat heading rather than a wind heading. + optimalHeading = + optimalHeading + GameState.getWindDirection(); + + setAutoPilot(optimalHeading); } } + /** + * Takes a given heading and rotates the boat towards that heading. + * This does not care about being upwind or downwind, just which direction will reach a given + * heading faster. + * + * @param newHeading The heading to turn the yacht towards. + */ + private void turnTowardsHeading(Double newHeading) { + Double newVal = heading - newHeading; + if (Math.floorMod(newVal.longValue(), 360L) > 180) { + adjustHeading(TURN_STEP / 5); + } else { + adjustHeading(-TURN_STEP / 5); + } + } + + /** + * Returns a heading normalized for the wind direction. Heading direction into the wind is 0, + * directly away is 180. + * + * @return The normalized heading accounting for wind direction. + */ private Double normalizeHeading() { Double normalizedHeading = heading - GameState.windDirection; normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L); @@ -654,6 +731,9 @@ public class Yacht extends Observable { this.colour = colour; } + public void toggleClientSail() { + clientSailsIn = !clientSailsIn; + } public Double getVelocity() { return velocity; @@ -667,13 +747,17 @@ public class Yacht extends Observable { return distanceToCurrentMark; } + public Boolean getClientSailsIn(){ + return clientSailsIn; + } + public void updateLocation(double lat, double lng, double heading, double velocity) { setLocation(lat, lng); this.heading = heading; this.velocity = velocity; updateVelocityProperty(velocity); for (YachtLocationListener yll : locationListeners) { - yll.notifyLocation(this, lat, lng, heading, velocity); + yll.notifyLocation(this, lat, lng, heading, velocity, clientSailsIn); } } diff --git a/src/main/java/seng302/model/mark/CompoundMark.java b/src/main/java/seng302/model/mark/CompoundMark.java index a4cc8d0c..fe5147de 100644 --- a/src/main/java/seng302/model/mark/CompoundMark.java +++ b/src/main/java/seng302/model/mark/CompoundMark.java @@ -15,7 +15,7 @@ public class CompoundMark { public CompoundMark(int markID, String name, List marks) { this.compoundMarkId = markID; - this.name = name; + this.name = name; this.marks.addAll(marks); if (marks.size() > 1) { this.midPoint = GeoUtility.getDirtyMidPoint(marks.get(0), marks.get(1)); diff --git a/src/main/java/seng302/model/mark/MarkOrder.java b/src/main/java/seng302/model/mark/MarkOrder.java index d49f2d5b..e8d6409a 100644 --- a/src/main/java/seng302/model/mark/MarkOrder.java +++ b/src/main/java/seng302/model/mark/MarkOrder.java @@ -1,5 +1,10 @@ package seng302.model.mark; +import java.io.IOException; +import java.io.StringReader; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; @@ -9,12 +14,6 @@ import seng302.model.stream.xml.generator.Race; import seng302.model.stream.xml.parser.RaceXMLData; import seng302.utilities.XMLGenerator; import seng302.utilities.XMLParser; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; -import java.io.StringReader; import java.util.*; /** @@ -33,7 +32,7 @@ public class MarkOrder { * @return An ordered list of marks in the race * OR null if the mark order could not be loaded */ - public List getMarkOrder(){ + public List getMarkOrder() { if (raceMarkOrder == null){ logger.warn("Race order accessed but not instantiated"); return null; @@ -53,10 +52,9 @@ public class MarkOrder { /** * @param currentSeqID The seqID of the current mark the boat is heading to * @return The mark last passed - * @throws IndexOutOfBoundsException if there is no next mark. - * Check seqID != 0 first + * @throws IndexOutOfBoundsException if there is no next mark. Check seqID != 0 first */ - public CompoundMark getPreviousMark(Integer currentSeqID) throws IndexOutOfBoundsException{ + public CompoundMark getPreviousMark(Integer currentSeqID) throws IndexOutOfBoundsException { return raceMarkOrder.get(currentSeqID - 1); } @@ -67,10 +65,10 @@ public class MarkOrder { /** * @param currentSeqID The seqID of the current mark the boat is heading to * @return The mark following the mark that the boat is heading to - * @throws IndexOutOfBoundsException if there is no next mark. - * Check using {@link #isLastMark(Integer)} + * @throws IndexOutOfBoundsException if there is no next mark. Check using {@link + * #isLastMark(Integer)} */ - public CompoundMark getNextMark(Integer currentSeqID) throws IndexOutOfBoundsException{ + public CompoundMark getNextMark(Integer currentSeqID) throws IndexOutOfBoundsException { return raceMarkOrder.get(currentSeqID + 1); } @@ -83,7 +81,7 @@ public class MarkOrder { * @param xml An AC35 RaceXML * @return An ordered list of marks in the race */ - private List loadRaceOrderFromXML(String xml){ + private List loadRaceOrderFromXML(String xml) { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db; diff --git a/src/main/java/seng302/model/stream/packets/PacketType.java b/src/main/java/seng302/model/stream/packets/PacketType.java index e7f55b49..1a38ae14 100644 --- a/src/main/java/seng302/model/stream/packets/PacketType.java +++ b/src/main/java/seng302/model/stream/packets/PacketType.java @@ -19,7 +19,9 @@ public enum PacketType { COURSE_WIND, AVG_WIND, BOAT_ACTION, - OTHER; + OTHER, + RACE_REGISTRATION_REQUEST, + RACE_REGISTRATION_RESPONSE; public static PacketType assignPacketType(int packetType, byte[] payload){ switch(packetType){ @@ -56,6 +58,10 @@ public enum PacketType { return AVG_WIND; case 100: return BOAT_ACTION; + case 101: + return RACE_REGISTRATION_REQUEST; + case 102: + return RACE_REGISTRATION_RESPONSE; default: } return OTHER; diff --git a/src/main/java/seng302/visualiser/ClientToServerThread.java b/src/main/java/seng302/visualiser/ClientToServerThread.java index 414696c8..9f76d37c 100644 --- a/src/main/java/seng302/visualiser/ClientToServerThread.java +++ b/src/main/java/seng302/visualiser/ClientToServerThread.java @@ -7,17 +7,28 @@ import java.io.OutputStream; import java.net.Socket; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Queue; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.zip.CRC32; import java.util.zip.Checksum; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; -import seng302.model.stream.packets.StreamPacket; +import javafx.scene.control.ButtonType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.gameServer.server.messages.BoatAction; import seng302.gameServer.server.messages.BoatActionMessage; +import seng302.gameServer.server.messages.ClientType; import seng302.gameServer.server.messages.Message; +import seng302.gameServer.server.messages.RegistrationRequestMessage; +import seng302.gameServer.server.messages.RegistrationResponseStatus; +import seng302.model.stream.packets.PacketType; +import seng302.model.stream.packets.StreamPacket; /** * A class describing a single connection to a Server for the purposes of sending and receiving on @@ -25,6 +36,8 @@ import seng302.gameServer.server.messages.Message; */ public class ClientToServerThread implements Runnable { + + /** * Functional interface for receiving packets from client socket. */ @@ -47,9 +60,17 @@ public class ClientToServerThread implements Runnable { private Socket socket; private InputStream is; - private OutputStream os; - private int clientId; + private Logger logger = LoggerFactory.getLogger(ClientToServerThread.class); + + //Output stream + private OutputStream os; + private Timer upWindPacketTimer = new Timer(); + private Timer downWindPacketTimer = new Timer(); + private boolean upwindTimerFlag = false, downwindTimerFlag = false; + static public final int PACKET_SENDING_INTERVAL_MS = 100; + + private int clientId = -1; // private Boolean updateClient = true; private ByteArrayOutputStream crcBuffer; @@ -71,15 +92,8 @@ public class ClientToServerThread implements Runnable { socket = new Socket(ipAddress, portNumber); is = socket.getInputStream(); os = socket.getOutputStream(); - Integer allocatedID = threeWayHandshake(); - if (allocatedID != null) { - clientId = allocatedID; - clientLog("Successful handshake. Allocated ID: " + clientId, 1); - } else { - clientLog("Unsuccessful handshake", 1); - closeSocket(); - return; - } + + sendRegistrationRequest(); thread = new Thread(this); thread.start(); @@ -128,15 +142,22 @@ public class ClientToServerThread implements Runnable { if (streamPackets.size() > 0) { streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload)); } else { - streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload)); - for (ClientSocketListener csl : listeners) - csl.newPacket(); + if (PacketType.RACE_REGISTRATION_RESPONSE == PacketType.assignPacketType(type, payload)){ + processRegistrationResponse(new StreamPacket(type, payloadLength, timeStamp, payload)); + } + else { + if (clientId == -1) continue; // Do not continue if not registered + streamPackets.add(new StreamPacket(type, payloadLength, timeStamp, payload)); + for (ClientSocketListener csl : listeners) + csl.newPacket(); + } } } else { clientLog("Packet has been dropped", 1); } } } catch (ByteReadException e) { + e.printStackTrace(); closeSocket(); Platform.runLater(() -> { Alert alert = new Alert(AlertType.ERROR); @@ -147,7 +168,6 @@ public class ClientToServerThread implements Runnable { clientLog(e.getMessage(), 1); return; } -// System.out.println("streamPackets = " + streamPackets.size()); } closeSocket(); clientLog("Closed connection to Server", 0); @@ -155,43 +175,127 @@ public class ClientToServerThread implements Runnable { /** - * Listens for an allocated sourceID and returns it to the server - * - * @return the sourceID allocated to us by the server + * Sends a request to the server asking for a source ID */ - private Integer threeWayHandshake() { - Integer ourSourceID = null; - while (true) { - try { - ourSourceID = is.read(); - } catch (IOException e) { - clientLog("Three way handshake failed", 1); - } - if (ourSourceID != null) { - try { - os.write(ourSourceID); - return ourSourceID; - } catch (IOException e) { - clientLog("Three way handshake failed", 1); - return null; - } - } + private void sendRegistrationRequest() { + RegistrationRequestMessage requestMessage = new RegistrationRequestMessage(ClientType.PLAYER); + + try { + os.write(requestMessage.getBuffer()); + } catch (IOException e) { + logger.error("Could not send registration request. Exiting"); + System.exit(1); } } - /** - * Send the post-start race course information - * @param boatActionMessage The message to send + * Accepts a response to the registration request message, and updates the client OR quits + * @param packet The registration requests packet */ - public void sendBoatActionMessage(BoatActionMessage boatActionMessage) { - try { - os.write(boatActionMessage.getBuffer()); - } catch (IOException e) { - clientLog("Could not write to server", 1); + private void processRegistrationResponse(StreamPacket packet){ + int sourceId = (int) Message.bytesToLong(Arrays.copyOfRange(packet.getPayload(), 0, 3)); + int statusCode = (int) Message.bytesToLong(Arrays.copyOfRange(packet.getPayload(), 4,5)); + + RegistrationResponseStatus status = RegistrationResponseStatus.getResponseStatus(statusCode); + + if (status.equals(RegistrationResponseStatus.SUCCESS_PLAYING)){ + clientId = sourceId; + + return; + } + + logger.error("Server Denied Connection, Exiting"); + + final String alertErrorText; + + if (status.equals(RegistrationResponseStatus.FAILURE_FULL)){ + alertErrorText = "Server is full"; + } + else{ + alertErrorText = "Could not connect to server"; + } + + Platform.runLater(() -> { + new Alert(AlertType.ERROR, alertErrorText, ButtonType.OK).showAndWait(); + System.exit(1); + }); + } + + /** + * Sends packets for the given boat action. Special cases are: \n + * - DOWNWIND = Packets are sent every ClientToServerThread.PACKET_SENDING_INTERVAL_MS + * - UPWIND = Packets are sent every ClientToServerThread.PACKET_SENDING_INTERVAL_MS + * - MAINTAIN_HEADING = DOWNWIND and UPWIND packets stop being sent. + * @param actionType The boat action that will dictate packets sent. + */ + public void sendBoatAction(BoatAction actionType) { + switch (actionType) { + case MAINTAIN_HEADING: + if (upwindTimerFlag) { + cancelTimer(upWindPacketTimer); + upwindTimerFlag = false; + upWindPacketTimer = new Timer(); + } + if (downwindTimerFlag) { + cancelTimer(downWindPacketTimer); + downwindTimerFlag = false; + downWindPacketTimer = new Timer(); + } + break; + case DOWNWIND: + if (!downwindTimerFlag) { + downwindTimerFlag = true; + downWindPacketTimer.scheduleAtFixedRate( + new TimerTask() { + @Override + public void run() { + sendBoatAction(new BoatActionMessage(BoatAction.DOWNWIND)); + } + }, 0, PACKET_SENDING_INTERVAL_MS + ); + } + break; + case UPWIND: + if (!upwindTimerFlag) { + upwindTimerFlag = true; + upWindPacketTimer.scheduleAtFixedRate( + new TimerTask() { + @Override + public void run() { + sendBoatAction(new BoatActionMessage(BoatAction.UPWIND)); + } + }, 0, PACKET_SENDING_INTERVAL_MS + ); + } + break; + default: + sendBoatAction(new BoatActionMessage(actionType)); + break; } } + /** + * Cancels a packet sending timer. + * @param timer The timer to cancel. + */ + private void cancelTimer (Timer timer) { + timer.cancel(); + timer.purge(); + } + + /** + * Sends a boat action of the given message type. + * @param message The given message type. + */ + private void sendBoatAction(BoatActionMessage message) { + if (clientId != -1) { + try { + os.write(message.getBuffer()); + } catch (IOException e) { + clientLog("Could not write to server", 1); + } + } + } private void closeSocket() { try { @@ -245,11 +349,8 @@ public class ClientToServerThread implements Runnable { } } - public Thread getThread() { - return thread; - } - public int getClientId () { return clientId; } + } diff --git a/src/main/java/seng302/visualiser/GameClient.java b/src/main/java/seng302/visualiser/GameClient.java index b33c68a8..f91ccbd8 100644 --- a/src/main/java/seng302/visualiser/GameClient.java +++ b/src/main/java/seng302/visualiser/GameClient.java @@ -13,6 +13,7 @@ import javafx.scene.Node; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Pane; import seng302.gameServer.MainServerThread; +import seng302.gameServer.server.messages.BoatAction; import seng302.model.RaceState; import seng302.model.Yacht; import seng302.model.stream.packets.StreamPacket; @@ -23,8 +24,6 @@ import seng302.model.stream.parser.RaceStatusData; import seng302.model.stream.parser.YachtEventData; import seng302.model.stream.xml.parser.RaceXMLData; import seng302.model.stream.xml.parser.RegattaXMLData; -import seng302.gameServer.server.messages.BoatActionMessage; -import seng302.gameServer.server.messages.BoatActionType; import seng302.utilities.StreamParser; import seng302.utilities.XMLParser; import seng302.visualiser.controllers.LobbyController; @@ -32,7 +31,8 @@ import seng302.visualiser.controllers.LobbyController.CloseStatus; import seng302.visualiser.controllers.RaceViewController; /** - * Created by cir27 on 20/07/17. + * This class is a client side instance of a yacht racing game in JavaFX. The game is instantiated + * with a JavaFX Pane to insert itself into. */ public class GameClient { @@ -49,13 +49,20 @@ public class GameClient { private ObservableList clientLobbyList = FXCollections.observableArrayList(); - private long lastSendingTime; - private int KEY_STROKE_SENDING_FREQUENCY = 50; - + /** + * Create an instance of the game client. Does not do anything until run with runAsClient() + * runAsHost(). + * @param holder The JavaFX Pane that the visual elements for the race will be inserted into. + */ public GameClient(Pane holder) { this.holderPane = holder; } + /** + * Connect to a game at the given address and starts the visualiser. + * @param ipAddress IP to connect to. + * @param portNumber Port to connect to. + */ public void runAsClient(String ipAddress, Integer portNumber) { try { socketThread = new ClientToServerThread(ipAddress, portNumber); @@ -63,6 +70,7 @@ public class GameClient { ioe.printStackTrace(); System.out.println("Unable to connect to host..."); } + socketThread.addStreamObserver(this::parsePackets); LobbyController lobbyController = loadLobby(); lobbyController.setPlayerListSource(clientLobbyList); @@ -71,6 +79,11 @@ public class GameClient { lobbyController.addCloseListener((exitCause) -> this.loadStartScreen()); } + /** + * Connect to a game as the host at the given address and starts the visualiser. + * @param ipAddress IP to connect to. + * @param portNumber Port to connect to. + */ public void runAsHost(String ipAddress, Integer portNumber) { server = new MainServerThread(); try { @@ -96,10 +109,8 @@ public class GameClient { private void loadStartScreen() { socketThread.setSocketToClose(); - socketThread = null; if (server != null) { - // TODO: 26/07/17 cir27 - handle disconnecting -// server.shutDown(); + server.terminate(); server = null; } FXMLLoader fxmlLoader = new FXMLLoader( @@ -182,12 +193,9 @@ public class GameClient { StreamParser.extractXmlMessage(packet) ); clientLobbyList.clear(); - allBoatsMap.forEach((id, boat) -> { - clientLobbyList.add(id + " " + boat.getBoatName()); -// System.out.println(id + " " + boat.getBoatName()); - - }); -// startRaceIfAllDataReceived(); + allBoatsMap.forEach((id, boat) -> + clientLobbyList.add(id + " " + boat.getBoatName()) + ); break; case RACE_START_STATUS: @@ -258,6 +266,8 @@ public class GameClient { private void processRaceStatusUpdate(RaceStatusData data) { if (allXMLReceived()) { raceState.updateState(data); + if (raceView != null) + raceView.getGameView().setWindDir(raceState.getWindDirection()); for (long[] boatData : data.getBoatData()) { Yacht yacht = allBoatsMap.get((int) boatData[0]); yacht.setEstimateTimeTillNextMark(raceState.getRaceTime() - boatData[1]); @@ -289,47 +299,36 @@ public class GameClient { * * @param e The key event triggering this call */ - public void keyPressed(KeyEvent e) { - BoatActionMessage boatActionMessage; - long currentTime = System.currentTimeMillis(); - if (currentTime - lastSendingTime > KEY_STROKE_SENDING_FREQUENCY) { - lastSendingTime = currentTime; - switch (e.getCode()) { - case SPACE: // align with vmg - boatActionMessage = new BoatActionMessage(BoatActionType.VMG); - socketThread.sendBoatActionMessage(boatActionMessage); - break; - case PAGE_UP: // upwind - boatActionMessage = new BoatActionMessage(BoatActionType.UPWIND); - socketThread.sendBoatActionMessage(boatActionMessage); - break; - case PAGE_DOWN: // downwind - boatActionMessage = new BoatActionMessage(BoatActionType.DOWNWIND); - socketThread.sendBoatActionMessage(boatActionMessage); - break; - case ENTER: // tack/gybe - boatActionMessage = new BoatActionMessage(BoatActionType.TACK_GYBE); - socketThread.sendBoatActionMessage(boatActionMessage); - break; - //TODO Allow a zoom in and zoom out methods - case Z: // zoom in - System.out.println("Zoom in"); - break; - case X: // zoom out - System.out.println("Zoom out"); - break; - } + private void keyPressed(KeyEvent e) { + switch (e.getCode()) { + case SPACE: // align with vmg + socketThread.sendBoatAction(BoatAction.VMG); break; + case PAGE_UP: // upwind + socketThread.sendBoatAction(BoatAction.UPWIND); break; + case PAGE_DOWN: // downwind + socketThread.sendBoatAction(BoatAction.DOWNWIND); break; + case ENTER: // tack/gybe + socketThread.sendBoatAction(BoatAction.TACK_GYBE); break; + //TODO Allow a zoom in and zoom out methods + case Z: // zoom in + System.out.println("Zoom in"); + break; + case X: // zoom out + System.out.println("Zoom out"); + break; } } - public void keyReleased(KeyEvent e) { + private void keyReleased(KeyEvent e) { switch (e.getCode()) { //TODO 12/07/17 Determine the sail state and send the appropriate packet (eg. if sails are in, send a sail out packet) case SHIFT: // sails in/sails out - BoatActionMessage boatActionMessage = new BoatActionMessage( - BoatActionType.SAILS_IN); - socketThread.sendBoatActionMessage(boatActionMessage); + socketThread.sendBoatAction(BoatAction.SAILS_IN); + raceView.getGameView().getPlayerYacht().toggleClientSail(); break; + case PAGE_UP: + case PAGE_DOWN: + socketThread.sendBoatAction(BoatAction.MAINTAIN_HEADING); break; } } diff --git a/src/main/java/seng302/visualiser/GameView.java b/src/main/java/seng302/visualiser/GameView.java index d6733af5..f042ddde 100644 --- a/src/main/java/seng302/visualiser/GameView.java +++ b/src/main/java/seng302/visualiser/GameView.java @@ -21,6 +21,9 @@ import javafx.geometry.Point2D; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.ScrollEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; @@ -64,6 +67,8 @@ public class GameView extends Pane { private double referencePointX, referencePointY; private double metersPerPixelX, metersPerPixelY; + final double SCALE_DELTA = 1.1; + private Text fpsDisplay = new Text(); private Polygon raceBorder = new CourseBoundary(); @@ -91,6 +96,26 @@ public class GameView extends Pane { private Double frameRate = 60.0; private int frameTimeIndex = 0; private boolean arrayFilled = false; + private Yacht playerYacht; + private double windDir = 0.0; + + double scaleFactor = 1; + + public void zoomOut() { + scaleFactor = 0.95; + for (Node child : getChildren()) { + child.setScaleX(child.getScaleX() * scaleFactor); + child.setScaleY(child.getScaleY() * scaleFactor); + } + } + + public void zoomIn() { + scaleFactor = 1.05; + for (Node child : getChildren()) { + child.setScaleX(child.getScaleX() * scaleFactor); + child.setScaleY(child.getScaleY() * scaleFactor); + } + } private enum ScaleDirection { HORIZONTAL, @@ -125,8 +150,7 @@ public class GameView extends Pane { if (lastTime == 0) { lastTime = now; } else { - if (now - lastTime >= (1e8 - / 60)) { //Fix for framerate going above 60 when minimized + if (now - lastTime >= (1e8 / 60)) { //Fix for framerate going above 60 when minimized long oldFrameTime = frameTimes[frameTimeIndex]; frameTimes[frameTimeIndex] = now; frameTimeIndex = (frameTimeIndex + 1) % frameTimes.length; @@ -147,7 +171,7 @@ public class GameView extends Pane { } } // Platform.runLater(() -> -// boatObjects.forEach((boat, boatObject) -> boatObject.updateLocation()) + boatObjects.forEach((boat, boatObject) -> boatObject.updateLocation()); // ); } }; @@ -337,10 +361,10 @@ public class GameView extends Pane { boatObjectGroup.getChildren().add(newBoat); trails.getChildren().add(newBoat.getTrail()); // TODO: 1/08/17 Make this less vile to look at. - yacht.addLocationListener((boat, lat, lon, heading, velocity) -> { + yacht.addLocationListener((boat, lat, lon, heading, velocity, sailIn) ->{ BoatObject bo = boatObjects.get(boat); Point2D p2d = findScaledXY(lat, lon); - bo.moveTo(p2d.getX(), p2d.getY(), heading, velocity); + bo.moveTo(p2d.getX(), p2d.getY(), heading, velocity, sailIn, windDir); // annotations.get(boat).setLayoutX(p2d.getX()); // annotations.get(boat).setLayoutY(p2d.getY()); // annotations.get(boat).setLocation(100d, 100d); @@ -512,7 +536,6 @@ public class GameView extends Pane { distanceFromReference = GeoUtility.getDistance( minLatPoint, new GeoPoint(unscaledLat, unscaledLon) ); -// System.out.println("distanceFromReference = " + distanceFromReference); if (angleFromReference >= 0 && angleFromReference <= Math.PI / 2) { xAxisLocation += Math .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); @@ -540,8 +563,6 @@ public class GameView extends Pane { if (horizontalInversion) { xAxisLocation = canvasWidth - bufferSize - (xAxisLocation - bufferSize); } -// System.out.println("yAxisLocation = " + yAxisLocation + " " + unscaledLat); -// System.out.println("xAxisLocation = " + xAxisLocation + " " + unscaledLon); return new Point2D(xAxisLocation, yAxisLocation); } @@ -593,11 +614,23 @@ public class GameView extends Pane { timer.stop(); } + + public void setWindDir(double windDir) { + this.windDir = windDir; + } + public void startRace() { timer.start(); } public void setBoatAsPlayer(Yacht playerYacht) { + public Yacht getPlayerYacht() { + return playerYacht; + } + + public void setBoatAsPlayer (Yacht playerYacht) { + this.playerYacht = playerYacht; + this.playerYacht.toggleClientSail(); boatObjects.get(playerYacht).setAsPlayer(); annotations.get(playerYacht).addAnnotation( "velocity", diff --git a/src/main/java/seng302/visualiser/controllers/LobbyController.java b/src/main/java/seng302/visualiser/controllers/LobbyController.java index 1a8b13e3..809bd3b5 100644 --- a/src/main/java/seng302/visualiser/controllers/LobbyController.java +++ b/src/main/java/seng302/visualiser/controllers/LobbyController.java @@ -4,15 +4,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import javafx.application.Platform; -import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Button; -import javafx.scene.control.ListView; +import javafx.scene.control.TextArea; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.layout.GridPane; import javafx.scene.text.Text; import seng302.gameServer.GameStages; import seng302.gameServer.GameState; @@ -33,28 +31,26 @@ public class LobbyController { void notify(CloseStatus exitCause); } - @FXML - private GridPane lobbyScreen; @FXML private Text lobbyIpText; @FXML private Button readyButton; @FXML - private ListView firstListView; + private TextArea playerOneTxt; @FXML - private ListView secondListView; + private TextArea playerTwoTxt; @FXML - private ListView thirdListView; + private TextArea playerThreeTxt; @FXML - private ListView fourthListView; + private TextArea playerFourTxt; @FXML - private ListView fifthListView; + private TextArea playerFiveTxt; @FXML - private ListView sixthListView; + private TextArea playerSixTxt; @FXML - private ListView seventhListView; + private TextArea playerSevenTxt; @FXML - private ListView eighthListView; + private TextArea playerEightTxt; @FXML private ImageView firstImageView; @FXML @@ -72,79 +68,67 @@ public class LobbyController { @FXML private ImageView eighthImageView; - private List> competitors = new ArrayList<>(); - private ObservableList firstCompetitor = FXCollections.observableArrayList(); - private ObservableList secondCompetitor = FXCollections.observableArrayList(); - private ObservableList thirdCompetitor = FXCollections.observableArrayList(); - private ObservableList fourthCompetitor = FXCollections.observableArrayList(); - private ObservableList fifthCompetitor = FXCollections.observableArrayList(); - private ObservableList sixthCompetitor = FXCollections.observableArrayList(); - private ObservableList seventhCompetitor = FXCollections.observableArrayList(); - private ObservableList eighthCompetitor = FXCollections.observableArrayList(); - private List imageViews = new ArrayList<>(); - private List listViews; + private List