diff --git a/.gitignore b/.gitignore index 3c15f041..213db0db 100644 --- a/.gitignore +++ b/.gitignore @@ -180,3 +180,7 @@ local.properties .recommenders/ Makefile + +infer-out/ +infer.txt +log.log \ No newline at end of file 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/doc/design_decisions.md b/doc/design_decisions.md index 65e68153..0c1f6f00 100644 --- a/doc/design_decisions.md +++ b/doc/design_decisions.md @@ -9,7 +9,7 @@ prints out event details, including time, involved boats and legs. - Configuration file -We decided to store the team information including team names and boat velocity, as well as race configuration setting in external file. +We decided to store the team information including team names and boat currentVelocity, as well as race configuration setting in external file. To read external files, "Json-simple" library has been used to parse information. By using this library, we did not have to write our json parser and benefited from the flexibility of json files. diff --git a/doc/user_manual.md b/doc/user_manual.md index 0ca79deb..27f09fc3 100644 --- a/doc/user_manual.md +++ b/doc/user_manual.md @@ -8,7 +8,7 @@ You can specify a config file using the using the -f flag, for example 'java -ja ## The config file -The teams/boats are specified in the config file under 'teams', each team requires a team name, and a velocity (in meters per second). +The teams/boats are specified in the config file under 'teams', each team requires a team name, and a currentVelocity (in meters per second). The 'time-scale' option lets you change how long the race takes to complete. A time-scale of 1.0 is normal speed, 2.0 is 2x etc. 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 ea8c1004..fc29f446 100644 --- a/src/main/java/seng302/gameServer/GameState.java +++ b/src/main/java/seng302/gameServer/GameState.java @@ -1,20 +1,51 @@ package seng302.gameServer; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import seng302.gameServer.server.messages.BoatActionType; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.gameServer.server.messages.BoatAction; +import seng302.gameServer.server.messages.BoatStatus; +import seng302.gameServer.server.messages.MarkRoundingMessage; +import seng302.gameServer.server.messages.MarkType; +import seng302.gameServer.server.messages.Message; +import seng302.gameServer.server.messages.RoundingBoatStatus; +import seng302.gameServer.server.messages.YachtEventCodeMessage; +import seng302.model.GeoPoint; import seng302.model.Player; -import seng302.model.Yacht; +import seng302.model.PolarTable; +import seng302.model.ServerYacht; +import seng302.model.mark.CompoundMark; +import seng302.model.mark.Mark; +import seng302.model.mark.MarkOrder; +import seng302.utilities.GeoUtility; /** * A Static class to hold information about the current state of the game (model) + * Also contains logic for updating itself on regular time intervals on its own thread * Created by wmu16 on 10/07/17. */ public class GameState implements Runnable { - private static Integer STATE_UPDATES_PER_SECOND = 60; + @FunctionalInterface + interface NewMessageListener { + void notify(Message message); + } + + private Logger logger = LoggerFactory.getLogger(GameState.class); + + private static final Integer STATE_UPDATES_PER_SECOND = 60; + public static Integer MAX_PLAYERS = 8; + public static Double ROUNDING_DISTANCE = 50d; // TODO: 14/08/17 wmu16 - Look into this value further + public static final Double MARK_COLLISION_DISTANCE = 15d; + public static final Double YACHT_COLLISION_DISTANCE = 25.0; + public static final Double BOUNCE_DISTANCE_MARK = 20.0; + public static final Double BOUNCE_DISTANCE_YACHT = 30.0; + public static final Double COLLISION_VELOCITY_PENALTY = 0.3; private static Long previousUpdateTime; public static Double windDirection; @@ -22,10 +53,14 @@ public class GameState implements Runnable { private static String hostIpAddress; private static List players; - private static Map yachts; + private static Map yachts; private static Boolean isRaceStarted; private static GameStages currentStage; + private static MarkOrder markOrder; private static long startTime; + private static Set marks; + + private static List markListeners; private static Map playerStringMap = new HashMap<>(); /* @@ -46,37 +81,44 @@ public class GameState implements Runnable { yachts = new HashMap<>(); players = new ArrayList<>(); GameState.hostIpAddress = hostIpAddress; - 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? + markListeners = new ArrayList<>(); - new Thread(this).start(); + new Thread(this).start(); //Run the auto updates on the game state + + marks = new MarkOrder().getAllMarks(); } public static String getHostIpAddress() { return hostIpAddress; } + public static Set getMarks(){ + return Collections.unmodifiableSet(marks); + } + public static List getPlayers() { return players; } 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); } - + public static void removePlayer(Player player) { players.remove(player); playerStringMap.remove(player); } - public static void addYacht(Integer sourceId, Yacht yacht) { + public static void addYacht(Integer sourceId, ServerYacht yacht) { yachts.put(sourceId, yacht); } @@ -93,13 +135,17 @@ public class GameState implements Runnable { } public static void setCurrentStage(GameStages currentStage) { - if (currentStage == GameStages.RACING){ + if (currentStage == GameStages.RACING) { startTime = System.currentTimeMillis(); } GameState.currentStage = currentStage; } + public static MarkOrder getMarkOrder() { + return markOrder; + } + public static long getStartTime(){ return startTime; } @@ -113,56 +159,17 @@ public class GameState implements Runnable { } public static Double getWindSpeedKnots() { - return windSpeed / 1000 * 1.943844492; // TODO: 26/07/17 cir27 - remove magic numbers + return GeoUtility.mmsToKnots(windSpeed); // TODO: 26/07/17 cir27 - remove magic numbers } - public static Map getYachts() { + public static Map getYachts() { return yachts; } - public static void updateBoat(Integer sourceId, BoatActionType actionType) { - Yacht playerYacht = yachts.get(sourceId); -// System.out.println("-----------------------"); - switch (actionType) { - case VMG: - playerYacht.turnToVMG(); -// System.out.println("Snapping to VMG"); - break; - case SAILS_IN: - playerYacht.toggleSailIn(); -// System.out.println("Toggling Sails"); - break; - case SAILS_OUT: - playerYacht.toggleSailIn(); -// System.out.println("Toggling Sails"); - break; - case TACK_GYBE: - playerYacht.tackGybe(windDirection); -// System.out.println("Tack/Gybe"); - break; - case UPWIND: - playerYacht.turnUpwind(); -// System.out.println("Moving upwind"); - break; - case DOWNWIND: - playerYacht.turnDownwind(); -// System.out.println("Moving downwind"); - break; - } - -// printBoatStatus(playerYacht); - } - - public void update() { - Long timeInterval = System.currentTimeMillis() - previousUpdateTime; - previousUpdateTime = System.currentTimeMillis(); - for (Yacht yacht : yachts.values()) { - yacht.update(timeInterval); - } - } /** * Generates a new ID based off the size of current players + 1 + * * @return a playerID to be allocated to a new connetion */ public static Integer getUniquePlayerID() { @@ -177,7 +184,7 @@ public class GameState implements Runnable { @Override public void run() { - while(true) { + while (true) { try { Thread.sleep(1000 / STATE_UPDATES_PER_SECOND); } catch (InterruptedException e) { @@ -187,20 +194,388 @@ public class GameState implements Runnable { update(); } - //RACING if (currentStage == GameStages.RACING) { update(); } } } - private static void printBoatStatus(Yacht playerYacht) { - System.out.println("-----------------------"); - System.out.println("Sails are in: " + playerYacht.getSailIn()); - System.out.println("Heading: " + playerYacht.getHeading()); - System.out.println("Velocity: " + playerYacht.getVelocityMMS() / 1000); - System.out.println("Lat: " + playerYacht.getLocation().getLat()); - System.out.println("Lng: " + playerYacht.getLocation().getLng()); - System.out.println("-----------------------\n"); + public static void updateBoat(Integer sourceId, BoatAction actionType) { + ServerYacht playerYacht = yachts.get(sourceId); + switch (actionType) { + case VMG: + playerYacht.turnToVMG(); + break; + case SAILS_IN: + playerYacht.toggleSailIn(); + break; + case SAILS_OUT: + playerYacht.toggleSailIn(); + break; + case TACK_GYBE: + playerYacht.tackGybe(windDirection); + break; + case UPWIND: + playerYacht.turnUpwind(); + break; + case DOWNWIND: + playerYacht.turnDownwind(); + break; + } + } + + + /** + * Called periodically in this GameState thread to update the GameState values + */ + public void update() { + Double timeInterval = (System.currentTimeMillis() - previousUpdateTime) / 1000000.0; + previousUpdateTime = System.currentTimeMillis(); + for (ServerYacht yacht : yachts.values()) { + updateVelocity(yacht); + yacht.runAutoPilot(); + yacht.updateLocation(timeInterval); + if (!yacht.getFinishedRace()) { + checkForCollision(yacht); + checkForLegProgression(yacht); + } + } + } + + + public static void checkForCollision(ServerYacht serverYacht) { + ServerYacht collidedYacht = checkCollision(serverYacht); + if (collidedYacht != null) { + GeoPoint originalLocation = serverYacht.getLocation(); + serverYacht.setLocation( + calculateBounceBack(serverYacht, originalLocation, BOUNCE_DISTANCE_YACHT) + ); + serverYacht.setCurrentVelocity( + serverYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY + ); + collidedYacht.setLocation( + calculateBounceBack(collidedYacht, originalLocation, BOUNCE_DISTANCE_YACHT) + ); + collidedYacht.setCurrentVelocity( + collidedYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY + ); + notifyMessageListeners( + new YachtEventCodeMessage(serverYacht.getSourceId()) + ); + } else { + Mark collidedMark = markCollidedWith(serverYacht); + if (collidedMark != null) { + serverYacht.setLocation( + calculateBounceBack(serverYacht, collidedMark, BOUNCE_DISTANCE_MARK) + ); + serverYacht.setCurrentVelocity( + serverYacht.getCurrentVelocity() * COLLISION_VELOCITY_PENALTY + ); + notifyMessageListeners( + new YachtEventCodeMessage(serverYacht.getSourceId()) + ); + } + } + } + + + private void updateVelocity(ServerYacht yacht) { + Double velocity = yacht.getCurrentVelocity(); + Double trueWindAngle = Math.abs(windDirection - yacht.getHeading()); + Double boatSpeedInKnots = PolarTable.getBoatSpeed(getWindSpeedKnots(), trueWindAngle); + Double maxBoatSpeed = GeoUtility.knotsToMMS(boatSpeedInKnots); + // TODO: 15/08/17 remove magic numbers from these equations. + if (yacht.getSailIn()) { + if (velocity < maxBoatSpeed - 500) { + yacht.changeVelocity(maxBoatSpeed / 100); + } else if (velocity > maxBoatSpeed + 500) { + yacht.changeVelocity(-maxBoatSpeed / 100); + } else { + yacht.setCurrentVelocity(maxBoatSpeed); + } + } else { + if (velocity > 3000) { + yacht.changeVelocity(-velocity / 200); + } else if (velocity > 100) { + yacht.changeVelocity(-velocity / 50); + } else if (velocity <= 100){ + yacht.setCurrentVelocity(0d); + } + } + } + + + /** + * Calculates the distance to the next mark (closest of the two if a gate mark). For purposes of + * mark rounding + * + * @return A distance in metres. Returns -1 if there is no next mark + * @throws IndexOutOfBoundsException If the next mark is null (ie the last mark in the race) + * Check first using {@link seng302.model.mark.MarkOrder#isLastMark(Integer)} + */ + private Double calcDistanceToCurrentMark(ServerYacht yacht) throws IndexOutOfBoundsException { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint location = yacht.getLocation(); + + if (currentMark.isGate()) { + Mark sub1 = currentMark.getSubMark(1); + Mark sub2 = currentMark.getSubMark(2); + Double distance1 = GeoUtility.getDistance(location, sub1); + Double distance2 = GeoUtility.getDistance(location, sub2); + if (distance1 < distance2) { + yacht.setClosestCurrentMark(sub1); + return distance1; + } else { + yacht.setClosestCurrentMark(sub2); + return distance2; + } + } else { + yacht.setClosestCurrentMark(currentMark.getSubMark(1)); + return GeoUtility.getDistance(location, currentMark.getSubMark(1)); + } + } + + + /** + * 4 Different cases of progression in the race 1 - Passing the start line 2 - Passing any + * in-race Gate 3 - Passing any in-race Mark 4 - Passing the finish line + * @param yacht the current yacht to check for progression + */ + private void checkForLegProgression(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + + Boolean hasProgressed; + if (currentMarkSeqID == 0) { + hasProgressed = checkStartLineCrossing(yacht); + } else if (markOrder.isLastMark(currentMarkSeqID)) { + hasProgressed = checkFinishLineCrossing(yacht); + } else if (currentMark.isGate()) { + hasProgressed = checkGateRounding(yacht); + } else { + hasProgressed = checkMarkRounding(yacht); + } + + if (hasProgressed) { + sendMarkRoundingMessage(yacht); + logMarkRounding(yacht); + yacht.setHasPassedLine(false); + yacht.setHasEnteredRoundingZone(false); + yacht.setHasPassedThroughGate(false); + if (!markOrder.isLastMark(currentMarkSeqID)) { + yacht.incrementMarkSeqID(); + } + } + } + + + /** + * If we pass the start line gate in the correct direction, progress + * + * @param yacht The current yacht to check for + */ + private Boolean checkStartLineCrossing(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint lastLocation = yacht.getLastLocation(); + GeoPoint location = yacht.getLocation(); + + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark nextMark = markOrder.getNextMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint()); + if (crossedLine == 2 && isClockwiseCross || crossedLine == 1 && !isClockwiseCross) { + yacht.setClosestCurrentMark(mark1); + yacht.setBoatStatus(BoatStatus.RACING); + return true; + } + } + + return false; + } + + + /** + * This algorithm checks for mark rounding. And increments the currentMarSeqID number attribute + * of the yacht if so. A visual representation of this algorithm can be seen on the Wiki under + * 'mark passing algorithm' + * + * @param yacht The current yacht to check for + */ + private Boolean checkMarkRounding(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint lastLocation = yacht.getLastLocation(); + GeoPoint location = yacht.getLocation(); + GeoPoint nextPoint = markOrder.getNextMark(currentMarkSeqID).getMidPoint(); + GeoPoint prevPoint = markOrder.getPreviousMark(currentMarkSeqID).getMidPoint(); + GeoPoint midPoint = GeoUtility.getDirtyMidPoint(nextPoint, prevPoint); + + if (calcDistanceToCurrentMark(yacht) < ROUNDING_DISTANCE) { + yacht.setHasEnteredRoundingZone(true); + } + + //In case current mark is a gate, loop through all marks just in case + for (Mark thisCurrentMark : currentMark.getMarks()) { + if (GeoUtility.isPointInTriangle(lastLocation, location, midPoint, thisCurrentMark)) { + yacht.setHasPassedLine(true); + } + } + + return yacht.hasPassedLine() && yacht.hasEnteredRoundingZone(); + } + + + /** + * Checks if a gate line has been crossed and in the correct direction + * + * @param yacht The current yacht to check for + */ + private Boolean checkGateRounding(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint lastLocation = yacht.getLastLocation(); + GeoPoint location = yacht.getLocation(); + + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = markOrder.getPreviousMark(currentMarkSeqID); + CompoundMark nextMark = markOrder.getNextMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + + //We have crossed the line + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + + //Check we cross the line in the correct direction + if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) { + yacht.setHasPassedThroughGate(true); + } + } + + Boolean prevMarkSide = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + Boolean nextMarkSide = GeoUtility.isClockwise(mark1, mark2, nextMark.getMidPoint()); + + if (yacht.hasPassedThroughGate()) { + //Check if we need to round this gate after passing through + if (prevMarkSide == nextMarkSide) { + return checkMarkRounding(yacht); + } else { + return true; + } + } + + return false; + } + + /** + * If we pass the finish gate in the correct direction + * + * @param yacht The current yacht to check for + */ + private Boolean checkFinishLineCrossing(ServerYacht yacht) { + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + GeoPoint lastLocation = yacht.getLastLocation(); + GeoPoint location = yacht.getLocation(); + + Mark mark1 = currentMark.getSubMark(1); + Mark mark2 = currentMark.getSubMark(2); + CompoundMark prevMark = markOrder.getPreviousMark(currentMarkSeqID); + + Integer crossedLine = GeoUtility.checkCrossedLine(mark1, mark2, lastLocation, location); + if (crossedLine > 0) { + Boolean isClockwiseCross = GeoUtility.isClockwise(mark1, mark2, prevMark.getMidPoint()); + if (crossedLine == 1 && isClockwiseCross || crossedLine == 2 && !isClockwiseCross) { + yacht.setClosestCurrentMark(mark1); + yacht.setIsFinished(true); + yacht.setBoatStatus(BoatStatus.FINISHED); + return true; + } + } + + return false; + } + + + private static Mark markCollidedWith(ServerYacht yacht) { + Set marksInRace = GameState.getMarks(); + for (Mark mark : marksInRace) { + if (GeoUtility.getDistance(yacht.getLocation(), mark) + <= MARK_COLLISION_DISTANCE) { + return mark; + } + } + return null; + } + + /** + * Calculate the new position of the boat after it has had a collision + * + * @return The boats new position + */ + private static GeoPoint calculateBounceBack(ServerYacht yacht, GeoPoint collidedWith, Double bounceDistance) { + Double heading = GeoUtility.getBearing(yacht.getLocation(), collidedWith); + // Invert heading + heading -= 180; + Integer newHeading = Math.floorMod(heading.intValue(), 360); + return GeoUtility.getGeoCoordinate(yacht.getLocation(), newHeading.doubleValue(), bounceDistance); + } + + /** + * Collision detection which iterates through all the yachts and check if any yacht collided + * with this yacht. Return collided yacht or null if no collision. + * + * @return yacht to compare to all other yachts. + */ + private static ServerYacht checkCollision(ServerYacht yacht) { + + for (ServerYacht otherYacht : GameState.getYachts().values()) { + if (otherYacht != yacht) { + Double distance = GeoUtility.getDistance(otherYacht.getLocation(), yacht.getLocation()); + if (distance < YACHT_COLLISION_DISTANCE) { + return otherYacht; + } + } + } + return null; + } + + private void sendMarkRoundingMessage(ServerYacht yacht) { + Integer sourceID = yacht.getSourceId(); + Integer currentMarkSeqID = yacht.getCurrentMarkSeqID(); + CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID); + MarkType markType = (currentMark.isGate()) ? MarkType.GATE : MarkType.ROUNDING_MARK; + Mark roundingMark = yacht.getClosestCurrentMark(); + + // TODO: 13/8/17 figure out the rounding side, rounded mark source ID and boat status. + Message markRoundingMessage = new MarkRoundingMessage(0, 0, + sourceID, RoundingBoatStatus.RACING, roundingMark.getRoundingSide(), markType, + roundingMark.getSourceID()); + + notifyMessageListeners(markRoundingMessage); + } + + private static void notifyMessageListeners(Message message) { + for (NewMessageListener mpl : markListeners) { + mpl.notify(message); + } + } + + private void logMarkRounding(ServerYacht yacht) { + Mark roundingMark = yacht.getClosestCurrentMark(); + logger.debug( + String.format("Yacht srcID(%d) passed Mark srcID(%d)", yacht.getSourceId(), + roundingMark.getSourceID())); + } + + + public static void addMarkPassListener(NewMessageListener listener) { + markListeners.add(listener); } } 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 6e827d95..691ccda3 100644 --- a/src/main/java/seng302/gameServer/MainServerThread.java +++ b/src/main/java/seng302/gameServer/MainServerThread.java @@ -4,16 +4,22 @@ import java.io.IOException; import java.net.ServerSocket; import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.Observable; import java.util.Timer; import java.util.TimerTask; +import seng302.gameServer.server.messages.Message; +import seng302.model.GeoPoint; import seng302.model.Player; +import seng302.model.PolarTable; +import seng302.model.ServerYacht; +import seng302.model.mark.CompoundMark; +import seng302.utilities.GeoUtility; +import seng302.visualiser.GameClient; /** * A class describing the overall server, which creates and collects server threads for each client * Created by wmu16 on 13/07/17. */ -public class MainServerThread extends Observable implements Runnable, ClientConnectionDelegate{ +public class MainServerThread implements Runnable, ClientConnectionDelegate { private static final int PORT = 4942; private static final Integer CLIENT_UPDATES_PER_SECOND = 10; @@ -25,13 +31,17 @@ public class MainServerThread extends Observable implements Runnable, ClientConn private ServerSocket serverSocket = null; private ArrayList serverToClientThreads = new ArrayList<>(); + 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")); + GameState.addMarkPassListener(this::broadcastMessage); terminated = false; thread = new Thread(this); thread.start(); @@ -82,32 +92,43 @@ public class MainServerThread extends Observable implements Runnable, ClientConn public void updateClients() { for (ServerToClientThread serverToClientThread : serverToClientThreads) { - serverToClientThread.updateClient(); + serverToClientThread.sendBoatLocationPackets(); + } + } + + private void broadcastMessage(Message message) { + for (ServerToClientThread serverToClientThread : serverToClientThreads) { + serverToClientThread.sendMessage(message); } } - static void serverLog(String message, int logLevel){ - if(logLevel <= LOG_LEVEL){ - System.out.println("[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message); + static void serverLog(String message, int logLevel) { + if (logLevel <= LOG_LEVEL) { + System.out.println( + "[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message); } } /** * A client has tried to connect to the server + * * @param serverToClientThread The player that connected */ @Override public void clientConnected(ServerToClientThread serverToClientThread) { serverLog("Player Connected From " + serverToClientThread.getThread().getName(), 0); serverToClientThreads.add(serverToClientThread); - this.addObserver(serverToClientThread); - setChanged(); - notifyObservers(); + serverToClientThread.addConnectionListener(() -> { + for (ServerToClientThread thread : serverToClientThreads) { + thread.sendSetupMessages(); + } + }); } /** * A player has left the game, remove the player from the GameState + * * @param player The player that left */ @Override @@ -120,16 +141,19 @@ 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(); } } - setChanged(); - notifyObservers(); + serverToClientThreads.remove(closedConnection); } public void startGame() { + initialiseBoatPositions(); Timer t = new Timer(); t.schedule(new TimerTask() { @@ -146,4 +170,56 @@ public class MainServerThread extends Observable implements Runnable, ClientConn public void terminate() { terminated = true; } + + /** + * Pass GameClient to main server thread so it can access the properties inside. + * + * @param gameClient gameClient + */ + public void setGameClient(GameClient gameClient) { + this.gameClient = gameClient; + } + + /** + * Initialise boats to specific spaced out geopoints behind starting line. + */ + private void initialiseBoatPositions() { + // Getting the start line compound marks + CompoundMark cm = gameClient.getCourseData().getCompoundMarks().get(1); + GeoPoint startMark1 = new GeoPoint(cm.getMarks().get(0).getLat(), + cm.getMarks().get(0).getLng()); + GeoPoint startMark2 = new GeoPoint(cm.getMarks().get(1).getLat(), + cm.getMarks().get(1).getLng()); + + // Calculating midpoint + Double perpendicularAngle = GeoUtility.getBearing(startMark1, startMark2); + Double length = GeoUtility.getDistance(startMark1, startMark2); + GeoPoint midpoint = GeoUtility.getGeoCoordinate(startMark1, perpendicularAngle, length / 2); + + // Setting each boats position side by side + double DISTANCE_FACTOR = 50.0; // distance apart in meters + int boatIndex = 0; + for (ServerYacht yacht : GameState.getYachts().values()) { + int distanceApart = boatIndex / 2; + + if (boatIndex % 2 == 1 && boatIndex != 0) { + distanceApart++; + distanceApart *= -1; + } + + GeoPoint spawnMark = GeoUtility + .getGeoCoordinate(midpoint, perpendicularAngle, distanceApart * DISTANCE_FACTOR); + + if (yacht.getHeading() < perpendicularAngle) { + spawnMark = GeoUtility + .getGeoCoordinate(spawnMark, perpendicularAngle + 90, DISTANCE_FACTOR); + } else { + spawnMark = GeoUtility + .getGeoCoordinate(spawnMark, perpendicularAngle + 270, DISTANCE_FACTOR); + } + + yacht.setLocation(spawnMark); + boatIndex++; + } + } } 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 ff2c4f23..bbe7053c 100644 --- a/src/main/java/seng302/gameServer/ServerToClientThread.java +++ b/src/main/java/seng302/gameServer/ServerToClientThread.java @@ -9,7 +9,6 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; import java.net.SocketException; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Observable; @@ -18,23 +17,28 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import java.util.zip.CRC32; import java.util.zip.Checksum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.gameServer.server.messages.YachtEventCodeMessage; 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; -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.ServerYacht; /** * A class describing a single connection to a Client for the purposes of sending and receiving on @@ -43,8 +47,15 @@ import seng302.gameServer.server.messages.XMLMessageSubType; */ public class ServerToClientThread implements Runnable, Observer { - private static final Integer LOG_LEVEL = 1; - private static final Integer MAX_ID_ATTEMPTS = 10; + /** + * Called to notify listeners when this thread receives a connection correctly. + */ + @FunctionalInterface + interface ConnectionListener { + void notifyConnection (); + } + + private Logger logger = LoggerFactory.getLogger(ServerToClientThread.class); private Thread thread; @@ -54,77 +65,103 @@ 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 ServerYacht yacht; + public ServerToClientThread(Socket socket) { this.socket = socket; - BufferedReader fn; - String fName = ""; - BufferedReader ln; - String lName = ""; - try { + seqNo = 0; + + try{ is = socket.getInputStream(); os = socket.getOutputStream(); - fn = new BufferedReader( - new InputStreamReader( - 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( - new InputStreamReader( - ServerToClientThread.class.getResourceAsStream( - "/server_config/CSV_Database_of_Last_Names.csv" - ) - ) - ); - all = ln.lines().collect(Collectors.toList()); - lName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); } catch (IOException e) { - serverLog("IO error in server thread upon grabbing streams", 1); - } - //Attempt threeway handshake with connection - sourceId = GameState.getUniquePlayerID(); - if (threeWayHandshake(sourceId)) { - serverLog("Successful handshake. Client allocated id: " + sourceId, 0); - Yacht 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; } - seqNo = 0; thread = new Thread(this); thread.start(); } - static void serverLog(String message, int logLevel) { - if (logLevel <= LOG_LEVEL) { - System.out.println( - "[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message); - } + private void setUpPlayer(){ + BufferedReader fn; + String fName = ""; + BufferedReader ln; + String lName = ""; + + fn = new BufferedReader( + new InputStreamReader( + 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( + new InputStreamReader( + ServerToClientThread.class.getResourceAsStream( + "/server_config/CSV_Database_of_Last_Names.csv" + ) + ) + ); + all = ln.lines().collect(Collectors.toList()); + lName = all.get(ThreadLocalRandom.current().nextInt(0, all.size())); + + ServerYacht yacht = new ServerYacht( + "Yacht", sourceId, sourceId.toString(), fName, fName + " " + lName, "NZ" + ); + + yacht.addObserver(this); // TODO: yacht can notify mark rounding message hyi25 13/8/17 + GameState.addYacht(sourceId, yacht); + GameState.addPlayer(new Player(socket, yacht)); } @Override public void update(Observable o, Object arg) { - sendSetupMessages(); + if (arg != null) { + sendMessage((Message) arg); + } else { + sendSetupMessages(); + } + } + + 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() { @@ -135,20 +172,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(); @@ -168,14 +191,21 @@ 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); + logger.warn("Packet has been dropped", 1); } } } catch (Exception e) { @@ -187,11 +217,11 @@ public class ServerToClientThread implements Runnable, Observer { } } - private void sendSetupMessages() { + public void sendSetupMessages() { xml = new XMLGenerator(); Race race = new Race(); - for (Yacht yacht : GameState.getYachts().values()) { + for (ServerYacht yacht : GameState.getYachts().values()) { race.addBoat(yacht); } @@ -213,44 +243,6 @@ public class ServerToClientThread implements Runnable, Observer { sendMessage(xmlMessage); } - 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() { try { socket.close(); @@ -259,7 +251,6 @@ public class ServerToClientThread implements Runnable, Observer { } } - private int readByte() throws Exception { int currentByte = -1; try { @@ -268,7 +259,7 @@ public class ServerToClientThread implements Runnable, Observer { crcBuffer.write(currentByte); } catch (IOException e) { e.printStackTrace(); - serverLog("Socket read failed", 1); + logger.warn("Socket read failed", 1); } if (currentByte == -1) { throw new Exception(); @@ -297,7 +288,7 @@ public class ServerToClientThread implements Runnable, Observer { //serverLog("Player " + sourceId + " side socket disconnected", 1); return; } catch (IOException e) { - serverLog("Message send failed", 1); + logger.warn("Message send failed", 1); } } @@ -307,10 +298,9 @@ 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()); + public void sendBoatLocationPackets() { + ArrayList yachts = new ArrayList<>(GameState.getYachts().values()); + for (ServerYacht yacht : yachts) { BoatLocationMessage boatLocationMessage = new BoatLocationMessage( yacht.getSourceId(), @@ -318,7 +308,7 @@ public class ServerToClientThread implements Runnable, Observer { yacht.getLocation().getLat(), yacht.getLocation().getLng(), yacht.getHeading(), - yacht.getVelocity().longValue()); + yacht.getCurrentVelocity().longValue()); sendMessage(boatLocationMessage); } @@ -331,24 +321,14 @@ public class ServerToClientThread implements Runnable, Observer { public void sendRaceStatusMessage() { // variables taken from GameServerThread - List boatSubMessages = new ArrayList<>(); - BoatStatus boatStatus; RaceStatus raceStatus; for (Player player : GameState.getPlayers()) { - Yacht y = player.getYacht(); - - if (GameState.getCurrentStage() == GameStages.PRE_RACE) { - boatStatus = BoatStatus.PRESTART; - } else if (GameState.getCurrentStage() == GameStages.RACING) { - boatStatus = BoatStatus.RACING; - } else { - boatStatus = BoatStatus.UNDEFINED; - } - - BoatSubMessage m = new BoatSubMessage(y.getSourceId(), boatStatus, 0, 0, 0, 1234l, - 1234l); + ServerYacht y = player.getYacht(); + BoatSubMessage m = new BoatSubMessage(y.getSourceId(), y.getBoatStatus(), 0, + 0, 0, 1234L, + 1234L); boatSubMessages.add(m); } @@ -366,4 +346,20 @@ public class ServerToClientThread implements Runnable, Observer { public Socket getSocket() { return socket; } + + public ServerYacht getYacht() { + return yacht; + } + + 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/MarkRoundingMessage.java b/src/main/java/seng302/gameServer/server/messages/MarkRoundingMessage.java index b683930e..f0d68e15 100644 --- a/src/main/java/seng302/gameServer/server/messages/MarkRoundingMessage.java +++ b/src/main/java/seng302/gameServer/server/messages/MarkRoundingMessage.java @@ -1,5 +1,7 @@ package seng302.gameServer.server.messages; +import seng302.gameServer.GameState; + public class MarkRoundingMessage extends Message{ private final long MESSAGE_VERSION_NUMBER = 1; private final int MESSAGE_SIZE = 21; @@ -18,14 +20,14 @@ public class MarkRoundingMessage extends Message{ * The purpose of this is to record the time when yachts cross marks * @param ackNumber ackNumber * @param raceId raceId - * @param sourceId sourceId + * @param sourceId boatSourceId * @param roundingBoatStatus roundingBoatStatus * @param roundingSide roundingSide * @param markId markId */ public MarkRoundingMessage(int ackNumber, int raceId, int sourceId, RoundingBoatStatus roundingBoatStatus, - RoundingSide roundingSide, int markId){ - this.time = System.currentTimeMillis() / 1000L; + RoundingSide roundingSide, MarkType markType, int markId) { + this.time = System.currentTimeMillis(); this.ackNumber = ackNumber; this.raceId = raceId; this.sourceId = sourceId; @@ -44,6 +46,7 @@ public class MarkRoundingMessage extends Message{ putInt((int) sourceId, 4); putByte((byte) boatStatus.getCode()); putByte((byte) roundingSide.getCode()); + putByte((byte) markType.getCode()); putByte((byte) markId); writeCRC(); 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/messages/RoundingSide.java b/src/main/java/seng302/gameServer/server/messages/RoundingSide.java index 6bb4c553..efb4c929 100644 --- a/src/main/java/seng302/gameServer/server/messages/RoundingSide.java +++ b/src/main/java/seng302/gameServer/server/messages/RoundingSide.java @@ -4,17 +4,49 @@ package seng302.gameServer.server.messages; * The side the boat rounded the mark */ public enum RoundingSide { - UNKNOWN(0), - PORT(1), - STARBOARD(2); + UNKNOWN(0, "Unknown"), + PORT(1, "Port"), + STARBOARD(2, "Stbd"), + SP(3, "SP"), + PS(4, "PS"); + private long code; + private String name; - RoundingSide(long code) { + RoundingSide(long code, String name) { this.code = code; + this.name = name; } public long getCode(){ return code; } + + public String getName() { + return name; + } + + public static RoundingSide getRoundingSide(String identifier) { + RoundingSide roundingSide = UNKNOWN; + switch (identifier) { + case "Unknown": + roundingSide = UNKNOWN; + break; + case "Port": + roundingSide = PORT; + break; + case "Stbd": + roundingSide = STARBOARD; + break; + case "SP": + roundingSide = SP; + break; + case "PS": + roundingSide = PS; + break; + } + + return roundingSide; + } } diff --git a/src/main/java/seng302/gameServer/server/messages/YachtEventCodeMessage.java b/src/main/java/seng302/gameServer/server/messages/YachtEventCodeMessage.java new file mode 100644 index 00000000..08db575a --- /dev/null +++ b/src/main/java/seng302/gameServer/server/messages/YachtEventCodeMessage.java @@ -0,0 +1,52 @@ +package seng302.gameServer.server.messages; + +/** + * Created by zyt10 on 10/08/17. + */ +public class YachtEventCodeMessage extends Message { + + private final MessageType MESSAGE_TYPE = MessageType.YACHT_EVENT_CODE; + private final int MESSAGE_VERSION = 1; //Always set to 1 + private final int MESSAGE_SIZE = 22; + + // Message fields + private long timeStamp; + private long ack = 0x00; //Unused + private int raceId; + private int destSourceId; + private int incidentId; + private int eventId; + + + public YachtEventCodeMessage(Integer subjectId) { + timeStamp = System.currentTimeMillis() / 1000L; + ack = 0; + raceId = 1; + destSourceId = subjectId; // collision boat source id + incidentId = 0; + eventId = 33; + + setHeader(new Header(MESSAGE_TYPE, 0x01, (short) getSize())); + allocateBuffer(); + writeHeaderToBuffer(); + + // Write message fields + putUnsignedByte((byte) MESSAGE_VERSION); + putInt((int) timeStamp, 6); + putInt((int) ack, 2); + putInt((int) raceId, 4); + putInt((int) destSourceId, 4); + putInt((int) incidentId, 4); + putInt((int) eventId, 1); + + writeCRC(); + rewind(); + } + + /** + * @return The length of this message + */ + public int getSize() { + return MESSAGE_SIZE; + } +} diff --git a/src/main/java/seng302/gameServer/server/simulator/Boat.java b/src/main/java/seng302/gameServer/server/simulator/Boat.java deleted file mode 100644 index c083dbf2..00000000 --- a/src/main/java/seng302/gameServer/server/simulator/Boat.java +++ /dev/null @@ -1,125 +0,0 @@ -package seng302.gameServer.server.simulator; - -import seng302.model.GeoPoint; -import seng302.utilities.GeoUtility; - -public class Boat { - - private int sourceID; - private double lat; - private double lng; - private double speed; // in mm/sec - private String boatName, shortName, shorterName; - private boolean isFinished; - private long estimatedTimeTillFinish; - - private Corner lastPassedCorner, headingCorner; - - public Boat(int sourceID, String boatName) { - this.sourceID = sourceID; - this.boatName = boatName; - this.isFinished = false; - estimatedTimeTillFinish = 0; - } - - /** - * Moves boat to the heading direction for a given time duration - * @param heading moving direction in degree. - * @param duration moving duration in millisecond. - */ - public void move(double heading, double duration) { - Double distance = speed * duration / 1000000; // convert mm to meter - GeoPoint originPos = new GeoPoint(lat, lng); - GeoPoint newPos = GeoUtility.getGeoCoordinate(originPos, heading, distance); - this.lat = newPos.getLat(); - this.lng = newPos.getLng(); - } - - public String toString() { - return String.format("Boat (%d): lat: %f, lng: %f", sourceID, lat, lng); - } - - public int getSourceID() { - return sourceID; - } - - public void setSourceID(int sourceID) { - this.sourceID = sourceID; - } - - public double getLat() { - return lat; - } - - public void setLat(double lat) { - this.lat = lat; - } - - public double getLng() { - return lng; - } - - public void setLng(double lng) { - this.lng = lng; - } - - public double getSpeed() { - return speed; - } - - public void setSpeed(double speed) { - this.speed = speed; - } - - public String getBoatName() { - return boatName; - } - - public void setBoatName(String boatName) { - this.boatName = boatName; - } - - public String getShortName() { - return shortName; - } - - public void setShortName(String shortName) { - this.shortName = shortName; - } - - public String getShorterName() { - return shorterName; - } - - public void setShorterName(String shorterName) { - this.shorterName = shorterName; - } - - public Corner getLastPassedCorner() { - return lastPassedCorner; - } - - public void setLastPassedCorner(Corner lastPassedCorner) { - this.lastPassedCorner = lastPassedCorner; - } - - public Corner getHeadingCorner() { - return headingCorner; - } - - public void setHeadingCorner(Corner headingCorner) { - this.headingCorner = headingCorner; - } - - public boolean isFinished() { - return isFinished; - } - - public void setFinished(boolean finished) { - isFinished = finished; - } - - public long getEstimatedTimeTillFinish(){ - return (long) (-getSpeed()) + System.currentTimeMillis(); - } -} diff --git a/src/main/java/seng302/gameServer/server/simulator/Corner.java b/src/main/java/seng302/gameServer/server/simulator/Corner.java deleted file mode 100644 index 11c26104..00000000 --- a/src/main/java/seng302/gameServer/server/simulator/Corner.java +++ /dev/null @@ -1,91 +0,0 @@ -package seng302.gameServer.server.simulator; - -import seng302.model.mark.CompoundMark; - -public class Corner { - - private int seqID; - private CompoundMark compoundMark; - //private int CompoundMarkID; - private RoundingType roundingType; - private int zoneSize; // size of the zone around a mark in boat-lengths. - - // TODO: this shouldn't be used in the future!!!! - private double bearingToNextCorner, distanceToNextCorner; - private Corner nextCorner; - - public Corner(int seqID, CompoundMark compoundMark, RoundingType roundingType, int zoneSize) { - this.seqID = seqID; - this.compoundMark = compoundMark; - this.roundingType = roundingType; - this.zoneSize = zoneSize; - } - - /** - * Prints out corner's info and its compound mark, good for testing - * @return a string showing its details - */ - @Override - public String toString() { - return String.format("Corner: %d - %s - %d, %s\n", - seqID, roundingType.getType(), zoneSize, compoundMark.toString()); - } - - public int getSeqID() { - return seqID; - } - - public void setSeqID(int seqID) { - this.seqID = seqID; - } - - public CompoundMark getCompoundMark() { - return compoundMark; - } - - public void setCompoundMark(CompoundMark compoundMark) { - this.compoundMark = compoundMark; - } - - public RoundingType getRoundingType() { - return roundingType; - } - - public void setRoundingType(RoundingType roundingType) { - this.roundingType = roundingType; - } - - public int getZoneSize() { - return zoneSize; - } - - public void setZoneSize(int zoneSize) { - this.zoneSize = zoneSize; - } - - - // TODO: next six setters & getters shouldn't be used in the future. - public double getBearingToNextCorner() { - return bearingToNextCorner; - } - - public void setBearingToNextCorner(double bearingToNextCorner) { - this.bearingToNextCorner = bearingToNextCorner; - } - - public double getDistanceToNextCorner() { - return distanceToNextCorner; - } - - public void setDistanceToNextCorner(double distanceToNextCorner) { - this.distanceToNextCorner = distanceToNextCorner; - } - - public Corner getNextCorner() { - return nextCorner; - } - - public void setNextCorner(Corner nextCorner) { - this.nextCorner = nextCorner; - } -} diff --git a/src/main/java/seng302/gameServer/server/simulator/RoundingType.java b/src/main/java/seng302/gameServer/server/simulator/RoundingType.java deleted file mode 100644 index aa1bca8e..00000000 --- a/src/main/java/seng302/gameServer/server/simulator/RoundingType.java +++ /dev/null @@ -1,43 +0,0 @@ -package seng302.gameServer.server.simulator; - -public enum RoundingType { - - // the mark should be rounded to port (boat's left) - PORT("Port"), - - // the mark should be rounded to starboard (boat's right) - STARBOARD("Stbd"), - - // the boat within the compound mark with the SeqID of 1 should be rounded - // to starboard and the boat within the compound mark with the SeqID of 2 - // should be rounded to port. - SP("SP"), - - // the opposite of SP - PS("PS"); - - private String type; - - RoundingType(String type) { - this.type = type; - } - - public String getType() { - return this.type; - } - - public static RoundingType typeOf(String type) { - switch (type) { - case "Port": - return PORT; - case "Stbd": - return STARBOARD; - case "SP": - return SP; - case "PS": - return PS; - default: - return null; - } - } -} diff --git a/src/main/java/seng302/gameServer/server/simulator/parsers/BoatsParser.java b/src/main/java/seng302/gameServer/server/simulator/parsers/BoatsParser.java deleted file mode 100644 index 17a46eae..00000000 --- a/src/main/java/seng302/gameServer/server/simulator/parsers/BoatsParser.java +++ /dev/null @@ -1,19 +0,0 @@ -package seng302.gameServer.server.simulator.parsers; - -import org.w3c.dom.Document; - - -/** - * Parses the race xml file to get course details - * Created by Haoming Yin (hyi25) on 16/3/2017 - */ -public class BoatsParser extends FileParser { - - private Document doc; - - public BoatsParser(String path) { - super(path); - this.doc = this.parseFile(); - } - -} diff --git a/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java b/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java deleted file mode 100644 index 66def8a1..00000000 --- a/src/main/java/seng302/gameServer/server/simulator/parsers/CourseParser.java +++ /dev/null @@ -1,117 +0,0 @@ -package seng302.gameServer.server.simulator.parsers; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -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; - -/** - * Parses the race xml file to get course details - * Created by Haoming Yin (hyi25) on 16/3/2017 - */ -public class CourseParser extends FileParser { - - private Document doc; - private Map compoundMarksMap; - - public CourseParser(String path) { - super(path); - this.doc = this.parseFile(); - } - - // TODO: should handle error / invalid file gracefully - protected List getCourse() { - compoundMarksMap = getCompoundMarks(doc.getDocumentElement()); - List corners = new ArrayList<>(); - NodeList cMarksSequence = doc.getElementsByTagName("Corner"); - - for (int i = 0; i < cMarksSequence.getLength(); i++) { - corners.add(getCorner(cMarksSequence.item(i))); - } - return corners; - } - - - private Corner getCorner(Node node) { - if (node.getNodeType() == Node.ELEMENT_NODE) { - Element e = (Element) node; - - Integer seqId = Integer.valueOf(e.getAttribute("SeqID")); - Integer cMarkId = Integer.valueOf(e.getAttribute("CompoundMarkID")); - CompoundMark cMark = compoundMarksMap.get(cMarkId); - RoundingType roundingType = RoundingType.typeOf(e.getAttribute("Rounding")); - Integer zoneSize = Integer.valueOf(e.getAttribute("ZoneSize")); - - return new Corner(seqId, cMark, roundingType, zoneSize); - } - return null; - } - - private Map getCompoundMarks(Node node) { - Map compoundMarksMap = new HashMap<>(); - - if (node.getNodeType() == Node.ELEMENT_NODE) { - Element element = (Element) node; - NodeList cMarks = element.getElementsByTagName("CompoundMark"); - - // loop through all compound marks who are the children of course node - for (int i = 0; i < cMarks.getLength(); i++) { - CompoundMark cMark = getCompoundMark(cMarks.item(i)); - if (cMark != null) - compoundMarksMap.put(cMark.getId(), cMark); - } - - return compoundMarksMap; - } - return null; - } - - - private CompoundMark getCompoundMark(Node node) { - if (node.getNodeType() == Node.ELEMENT_NODE) { - Element e = (Element) node; - Integer markID = Integer.valueOf(e.getAttribute("CompoundMarkID")); - - String name = e.getAttribute("Name"); - CompoundMark cMark = new CompoundMark(markID, name); - - NodeList marks = e.getElementsByTagName("Mark"); - for (int i = 0; i < marks.getLength(); i++) { - Mark mark = getMark(marks.item(i)); - if (mark != null) - cMark.addSubMarks(mark); - } - return cMark; - } - System.out.println("Failed to create compound mark."); - return null; - } - - - private Mark getMark(Node node) { - if (node.getNodeType() == Node.ELEMENT_NODE) { - Element e = (Element) node; - Integer seqId = Integer.valueOf(e.getAttribute("SeqID")); - String name = e.getAttribute("Name"); - Double lat = Double.valueOf(e.getAttribute("TargetLat")); - Double lng = Double.valueOf(e.getAttribute("TargetLng")); - Integer sourceId = Integer.valueOf(e.getAttribute("SourceID")); - - Mark mark = new Mark(name, lat, lng, sourceId); - mark.setSeqID(seqId); - - return mark; - } - System.out.println("Failed to create mark."); - return null; - } - -} diff --git a/src/main/java/seng302/gameServer/server/simulator/parsers/FileParser.java b/src/main/java/seng302/gameServer/server/simulator/parsers/FileParser.java deleted file mode 100644 index 87021893..00000000 --- a/src/main/java/seng302/gameServer/server/simulator/parsers/FileParser.java +++ /dev/null @@ -1,51 +0,0 @@ -package seng302.gameServer.server.simulator.parsers; - -import java.io.InputStream; -import java.io.StringReader; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import org.w3c.dom.Document; -import org.xml.sax.InputSource; - -/** - * Created by Haoming Yin (hyi25) on 16/3/2017 - */ -public abstract class FileParser { - - private String filePath; - - public FileParser() {} - - public FileParser(String path) { - this.filePath = path; - } - - protected Document parseFile() { - try { - InputStream is = getClass().getResourceAsStream(this.filePath); - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(is); - // optional, in order to recover info from broken line. - doc.getDocumentElement().normalize(); - return doc; - } catch (Exception e) { - System.out.println("[FileParser] Exception"); - return null; - } - } - - protected Document parseFile(String xmlString) { - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(new InputSource(new StringReader(xmlString))); - // optional, in order to recover info from broken line. - doc.getDocumentElement().normalize(); - return doc; - } catch (Exception e) { - System.out.println("[FileParser] Exception"); - } - return null; - } -} diff --git a/src/main/java/seng302/gameServer/server/simulator/parsers/RaceParser.java b/src/main/java/seng302/gameServer/server/simulator/parsers/RaceParser.java deleted file mode 100644 index 960ff8fc..00000000 --- a/src/main/java/seng302/gameServer/server/simulator/parsers/RaceParser.java +++ /dev/null @@ -1,65 +0,0 @@ -package seng302.gameServer.server.simulator.parsers; - -import java.util.ArrayList; -import java.util.List; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import seng302.gameServer.server.simulator.Boat; -import seng302.gameServer.server.simulator.Corner; - -/** - * Parses the race xml file to get course details - * Created by Haoming Yin (hyi25) on 16/3/2017 - */ -public class RaceParser extends FileParser { - - private Document doc; - private String path; - - public RaceParser(String path) { - super(path); - this.path = path; - this.doc = this.parseFile(); - } - - /** - * Parses race.xml file and returns a list of corner which is the race course. - * @return a list of ordered corner to represent the course. - */ - public List getCourse() { - CourseParser cp = new CourseParser(path); - return cp.getCourse(); - } - - /** - * Parses race.xml file and return a list of boats which will compete in the - * race. - * @return a list of boats that are going to compete in the race. - */ - public List getBoats() { - NodeList yachts = doc.getDocumentElement().getElementsByTagName("Yacht"); - List boats = new ArrayList<>(); - - for (int i = 0; i < yachts.getLength(); i++) { - boats.add(getBoat(yachts.item(i))); - } - return boats; - } - - /** - * Parses a single boat from the given node - * @param node a node within a boat tag - * @return a boat instance parsed from the given node - */ - private Boat getBoat(Node node) { - if (node.getNodeType() == Node.ELEMENT_NODE) { - Element e = (Element) node; - - Integer sourceId = Integer.valueOf(e.getAttribute("SourceID")); - return new Boat(sourceId, "Test Boat"); - } - return null; - } -} diff --git a/src/main/java/seng302/model/ClientYacht.java b/src/main/java/seng302/model/ClientYacht.java new file mode 100644 index 00000000..564754b0 --- /dev/null +++ b/src/main/java/seng302/model/ClientYacht.java @@ -0,0 +1,268 @@ +package seng302.model; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Observable; +import java.util.Observer; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.ReadOnlyLongWrapper; +import javafx.scene.paint.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.model.mark.CompoundMark; + +/** + * Yacht class for the racing boat.

Class created to store more variables (eg. boat statuses) + * compared to the XMLParser boat class, also done outside Boat class because some old variables are + * not used anymore. + */ +public class ClientYacht extends Observable { + + @FunctionalInterface + public interface YachtLocationListener { + void notifyLocation(ClientYacht clientYacht, double lat, double lon, double heading, + Boolean sailsIn, double velocity); + } + + private Logger logger = LoggerFactory.getLogger(ClientYacht.class); + + + //BOTH AFAIK + private String boatType; + private Integer sourceId; + private String hullID; //matches HullNum in the XML spec. + private String shortName; + private String boatName; + private String country; + + private Long estimateTimeAtFinish; + private Boolean sailIn = false; + private Integer currentMarkSeqID = 0; + private Long markRoundTime; + private Long timeTillNext; + private Double heading; + private Integer legNumber = 0; + private GeoPoint location; + private Integer boatStatus; + private Double currentVelocity; + + //CLIENT SIDE + private List locationListeners = new ArrayList<>(); + private ReadOnlyDoubleWrapper velocityProperty = new ReadOnlyDoubleWrapper(); + private ReadOnlyLongWrapper timeTillNextProperty = new ReadOnlyLongWrapper(); + private ReadOnlyLongWrapper timeSinceLastMarkProperty = new ReadOnlyLongWrapper(); + private CompoundMark lastMarkRounded; + private Integer positionInt = 0; + private Color colour; + + public ClientYacht(String boatType, Integer sourceId, String hullID, String shortName, + String boatName, String country) { + this.boatType = boatType; + this.sourceId = sourceId; + this.hullID = hullID; + this.shortName = shortName; + this.boatName = boatName; + this.country = country; + this.location = new GeoPoint(57.670341, 11.826856); + this.heading = 120.0; //In degrees + this.currentVelocity = 0d; + this.boatStatus = 1; + } + + /** + * Add ServerToClientThread as the observer, this observer pattern mainly server for the boat + * rounding package. + */ + @Override + public void addObserver(Observer o) { + super.addObserver(o); + } + + public String getBoatType() { + return boatType; + } + + public Integer getSourceId() { + //@TODO Remove and merge with Creating Game Loop + if (sourceId == null) { + return 0; + } + return sourceId; + } + + public String getHullID() { + if (hullID == null) { + return ""; + } + return hullID; + } + + public String getShortName() { + return shortName; + } + + public String getBoatName() { + return boatName; + } + + public String getCountry() { + if (country == null) { + return ""; + } + return country; + } + + public Integer getBoatStatus() { + return boatStatus; + } + + public void setBoatStatus(Integer boatStatus) { + this.boatStatus = boatStatus; + } + + public Integer getLegNumber() { + return legNumber; + } + + public void setLegNumber(Integer legNumber) { + this.legNumber = legNumber; + } + + public void setEstimateTimeTillNextMark(Long estimateTimeTillNextMark) { + timeTillNext = estimateTimeTillNextMark; + } + + public String getEstimateTimeAtFinish() { + DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + return format.format(estimateTimeAtFinish); + } + + public void setEstimateTimeAtFinish(Long estimateTimeAtFinish) { + this.estimateTimeAtFinish = estimateTimeAtFinish; + } + + public Integer getPositionInteger() { + return positionInt; + } + + public void setPositionInteger(Integer position) { + this.positionInt = position; + } + + public void updateVelocityProperty(double velocity) { + this.velocityProperty.set(velocity); + } + + public void setMarkRoundingTime(Long markRoundingTime) { + this.markRoundTime = markRoundingTime; + } + + public ReadOnlyDoubleProperty getVelocityProperty() { + return velocityProperty.getReadOnlyProperty(); + } + + public ReadOnlyLongProperty timeTillNextProperty() { + return timeTillNextProperty.getReadOnlyProperty(); + } + + public Long getTimeTillNext() { + return timeTillNext; + } + + public Long getMarkRoundTime() { + return markRoundTime; + } + + public CompoundMark getLastMarkRounded() { + return lastMarkRounded; + } + + public void setLastMarkRounded(CompoundMark lastMarkRounded) { + this.lastMarkRounded = lastMarkRounded; + } + + public GeoPoint getLocation() { + return location; + } + + public void toggleSail() { + sailIn = !sailIn; + } + //// TODO: 15/08/17 asd + + /** + * Sets the current location of the boat in lat and long whilst preserving the last location + * + * @param lat Latitude + * @param lng Longitude + */ + public void setLocation(Double lat, Double lng) { + location.setLat(lat); + location.setLng(lng); + } + + public Double getHeading() { + return heading; + } + + public void setHeading(Double heading) { + this.heading = heading; + } + + @Override + public String toString() { + return boatName; + } + + public void updateTimeSinceLastMarkProperty(long timeSinceLastMark) { + this.timeSinceLastMarkProperty.set(timeSinceLastMark); + } + + public ReadOnlyLongProperty timeSinceLastMarkProperty() { + return timeSinceLastMarkProperty.getReadOnlyProperty(); + } + + public void setTimeTillNext(Long timeTillNext) { + this.timeTillNext = timeTillNext; + } + + + public Color getColour() { + return colour; + } + + public void setColour(Color colour) { + this.colour = colour; + } + +// public Double getCurrentVelocity() { +// return currentVelocity; +// } +// +// public void setCurrentVelocity(Double currentVelocity) { +// this.currentVelocity = currentVelocity; +// } + + + public void updateLocation(double lat, double lng, double heading, double velocity) { + setLocation(lat, lng); + this.heading = heading; +// this.currentVelocity = velocity; + updateVelocityProperty(velocity); + for (YachtLocationListener yll : locationListeners) { + yll.notifyLocation(this, lat, lng, heading, sailIn, velocity); + } + } + + public void addLocationListener(YachtLocationListener listener) { + locationListeners.add(listener); + } + + public boolean getSailIn () { + return sailIn; + } +} diff --git a/src/main/java/seng302/model/Colors.java b/src/main/java/seng302/model/Colors.java index 72ff3ba5..81829262 100644 --- a/src/main/java/seng302/model/Colors.java +++ b/src/main/java/seng302/model/Colors.java @@ -6,12 +6,12 @@ import javafx.scene.paint.Color; * Enum for generating colours. */ public enum Colors { - RED, PERU, SEAGREEN, GREEN, BLUE, PURPLE; + RED, PERU, GOLD, GREEN, BLUE, PURPLE, DEEPPINK, GRAY; static Integer index = 0; public static Color getColor() { - if (index == 6) { + if (index == 8) { index = 0; } return Color.valueOf(values()[index++].toString()); diff --git a/src/main/java/seng302/model/GeoPoint.java b/src/main/java/seng302/model/GeoPoint.java index 29938946..91937663 100644 --- a/src/main/java/seng302/model/GeoPoint.java +++ b/src/main/java/seng302/model/GeoPoint.java @@ -28,4 +28,9 @@ public class GeoPoint { public void setLng(double lng) { this.lng = lng; } + + @Override + public String toString() { + return "lat: " + lat + " lng: " + lng; + } } diff --git a/src/main/java/seng302/model/Player.java b/src/main/java/seng302/model/Player.java index 175b7a45..1ed2b6dc 100644 --- a/src/main/java/seng302/model/Player.java +++ b/src/main/java/seng302/model/Player.java @@ -9,11 +9,11 @@ import java.net.Socket; public class Player { private Socket socket; - private Yacht yacht; + private ServerYacht yacht; private Integer lastMarkPassed; - public Player(Socket socket, Yacht yacht) { + public Player(Socket socket, ServerYacht yacht) { this.socket = socket; this.yacht = yacht; } @@ -30,7 +30,7 @@ public class Player { this.lastMarkPassed = lastMarkPassed; } - public Yacht getYacht() { + public ServerYacht getYacht() { return yacht; } 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/ServerYacht.java b/src/main/java/seng302/model/ServerYacht.java new file mode 100644 index 00000000..2a8a8935 --- /dev/null +++ b/src/main/java/seng302/model/ServerYacht.java @@ -0,0 +1,395 @@ +package seng302.model; + +import java.util.HashMap; +import java.util.Observable; +import java.util.Observer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import seng302.gameServer.GameState; +import seng302.gameServer.server.messages.BoatStatus; +import seng302.model.mark.Mark; +import seng302.utilities.GeoUtility; + +/** + * Yacht class for the racing boat.

Class created to store more variables (eg. boat statuses) + * compared to the XMLParser boat class, also done outside Boat class because some old variables are + * not used anymore. + */ +public class ServerYacht extends Observable { + + private Logger logger = LoggerFactory.getLogger(ClientYacht.class); + + public static final Double TURN_STEP = 5.0; + + //Boat info + private String boatType; + private Integer sourceId; + private String hullID; //matches HullNum in the XML spec. + private String shortName; + private String boatName; + private String country; + private BoatStatus boatStatus; + + + //Location + private Double lastHeading; + private Boolean sailIn; + private Double heading; + private GeoPoint lastLocation; + private GeoPoint location; + private Double currentVelocity; + private Boolean isAuto; + private Double autoHeading; + + //Mark Rounding + private Integer currentMarkSeqID = 0; + private Boolean hasEnteredRoundingZone; + private Mark closestCurrentMark; + private Boolean hasPassedLine; + private Boolean hasPassedThroughGate; + private Boolean finishedRace; + + + public ServerYacht(String boatType, Integer sourceId, String hullID, String shortName, + String boatName, String country) { + this.boatType = boatType; + this.boatStatus = BoatStatus.PRESTART; + this.sourceId = sourceId; + this.hullID = hullID; + this.shortName = shortName; + 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 + this.currentVelocity = 0d; //in mms-1 + + this.hasEnteredRoundingZone = false; + this.hasPassedLine = false; + this.hasPassedThroughGate = false; + this.finishedRace = false; + } + + + /** + * Changes the boats current currentVelocity by a set amount, positive or negative + * + * @param velocityChange The ammount to change the currentVelocity by, in mms-1 + */ + public void changeVelocity(Double velocityChange) { + currentVelocity += velocityChange; + } + + /** + * Updates the boat to a new GeoPoint whilst preserving the last location + * + * @param secondsElapsed The seconds elapsed since the last update of this yacht + */ + public void updateLocation(Double secondsElapsed) { + lastLocation = location; + location = GeoUtility.getGeoCoordinate(location, heading, currentVelocity * secondsElapsed); + } + + public void setLocation(GeoPoint geoPoint) { + location = geoPoint; + } + + /** + * Add ServerToClientThread as the observer, this observer pattern mainly server for the boat + * rounding package. + */ + @Override + public void addObserver(Observer o) { + super.addObserver(o); + } + + /** + * 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) { + 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() { + sailIn = !sailIn; + } + + public void turnUpwind() { + disableAutoPilot(); + Double normalizedHeading = normalizeHeading(); + if (normalizedHeading == 0) { + if (lastHeading < 180) { + adjustHeading(-TURN_STEP); + } else { + adjustHeading(TURN_STEP); + } + } else if (normalizedHeading == 180) { + if (lastHeading < 180) { + adjustHeading(TURN_STEP); + } else { + adjustHeading(-TURN_STEP); + } + } else if (normalizedHeading < 180) { + adjustHeading(-TURN_STEP); + } else { + adjustHeading(TURN_STEP); + } + } + + public void turnDownwind() { + disableAutoPilot(); + Double normalizedHeading = normalizeHeading(); + if (normalizedHeading == 0) { + if (lastHeading < 180) { + adjustHeading(TURN_STEP); + } else { + adjustHeading(-TURN_STEP); + } + } else if (normalizedHeading == 180) { + if (lastHeading < 180) { + adjustHeading(-TURN_STEP); + } else { + adjustHeading(TURN_STEP); + } + } else if (normalizedHeading < 180) { + adjustHeading(TURN_STEP); + } else { + adjustHeading(-TURN_STEP); + } + } + + /** + * 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() { + if (isAuto) { + disableAutoPilot(); + } else { + Double normalizedHeading = normalizeHeading(); + Double optimalHeading; + HashMap optimalPolarMap; + + if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind + optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots()); + } else { + 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); + return normalizedHeading; + } + + public Integer getSourceId() { + //@TODO Remove and merge with Creating Game Loop + if (sourceId == null) { + return 0; + } + return sourceId; + } + + // TODO: 15/08/17 This method is implicitly called from the XML generator for boats DO NOT DELETE + public String getHullID() { + if (hullID == null) { + return ""; + } + return hullID; + } + + // TODO: 15/08/17 This method is implicitly called from the XML generator for boats DO NOT DELETE + public String getShortName() { + return shortName; + } + + public String getBoatName() { + return boatName; + } + + public String getCountry() { + if (country == null) { + return ""; + } + return country; + } + + + public GeoPoint getLocation() { + return location; + } + + + public Double getHeading() { + return heading; + } + + public void setHeading(Double heading) { + this.heading = heading; + } + + public Boolean getSailIn() { + return sailIn; + } + + @Override + public String toString() { + return boatName; + } + + + public void setIsFinished(Boolean isFinished) { + finishedRace = isFinished; + } + + public Boolean getFinishedRace() { + return finishedRace; + } + + public Double getCurrentVelocity() { + return currentVelocity; + } + + public void setCurrentVelocity(Double currentVelocity) { + this.currentVelocity = currentVelocity; + } + + public Integer getCurrentMarkSeqID() { + return currentMarkSeqID; + } + + public GeoPoint getLastLocation() { + return lastLocation; + } + + public Mark getClosestCurrentMark() { + return closestCurrentMark; + } + + public void setClosestCurrentMark(Mark closestCurrentMark) { + this.closestCurrentMark = closestCurrentMark; + } + + public void setHasEnteredRoundingZone(Boolean hasEnteredRoundingZone) { + this.hasEnteredRoundingZone = hasEnteredRoundingZone; + } + + public void setHasPassedLine(Boolean hasPassedLine) { + this.hasPassedLine = hasPassedLine; + } + + public void setHasPassedThroughGate(Boolean hasPassedThroughGate) { + this.hasPassedThroughGate = hasPassedThroughGate; + } + + public BoatStatus getBoatStatus() { + return boatStatus; + } + + public void setBoatStatus(BoatStatus boatStatus) { + this.boatStatus = boatStatus; + } + + public void incrementMarkSeqID() { + currentMarkSeqID++; + } + + public Boolean hasEnteredRoundingZone() { + return hasEnteredRoundingZone; + } + + public Boolean hasPassedThroughGate() { + return hasPassedThroughGate; + } + + public Boolean hasPassedLine() { + return hasPassedLine; + } + + +} diff --git a/src/main/java/seng302/model/Yacht.java b/src/main/java/seng302/model/Yacht.java deleted file mode 100644 index d7cd1f9e..00000000 --- a/src/main/java/seng302/model/Yacht.java +++ /dev/null @@ -1,449 +0,0 @@ -package seng302.model; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.ReadOnlyDoubleWrapper; -import javafx.beans.property.ReadOnlyLongProperty; -import javafx.beans.property.ReadOnlyLongWrapper; -import javafx.scene.paint.Color; -import seng302.gameServer.GameState; -import seng302.model.mark.CompoundMark; -import seng302.model.mark.Mark; -import seng302.utilities.GeoUtility; - -/** - * Yacht class for the racing boat. - * - * Class created to store more variables (eg. boat statuses) compared to the XMLParser boat class, - * also done outside Boat class because some old variables are not used anymore. - */ -public class Yacht { - - @FunctionalInterface - public interface YachtLocationListener { - void notifyLocation(Yacht yacht, double lat, double lon, double heading, double velocity); - } - - private static final Double ROUNDING_DISTANCE = 15d; // TODO: 3/08/17 wmu16 - Look into this value further - - //BOTH AFAIK - private String boatType; - private Integer sourceId; - private String hullID; //matches HullNum in the XML spec. - private String shortName; - private String boatName; - private String country; - - private Long estimateTimeAtFinish; - private Long lastMark; - private Long markRoundTime; - private Double distanceToNextMark; - private Long timeTillNext; - private CompoundMark nextMark; - private Double heading; - private Integer legNumber = 0; - - //SERVER SIDE - private final Double TURN_STEP = 5.0; - private Double lastHeading; - private Boolean sailIn; - private GeoPoint location; - private Integer boatStatus; - private Double velocity; - //MARK ROUNDING INFO - private GeoPoint lastLocation; //For purposes of mark rounding calculations - private Boolean hasEnteredRoundingZone; //The distance that the boat must be from the mark to round - private Boolean hasPassedFirstLine; //The line extrapolated from the next mark to the current mark - private Boolean hasPassedSecondLine; //The line extrapolated from the last mark to the current mark - - //CLIENT SIDE - private List locationListeners = new ArrayList<>(); - private ReadOnlyDoubleWrapper velocityProperty = new ReadOnlyDoubleWrapper(); - private ReadOnlyLongWrapper timeTillNextProperty = new ReadOnlyLongWrapper(); - private ReadOnlyLongWrapper timeSinceLastMarkProperty = new ReadOnlyLongWrapper(); - private CompoundMark lastMarkRounded; - private Integer positionInt = 0; - private Color colour; - - public Yacht(String boatType, Integer sourceId, String hullID, String shortName, - String boatName, String country) { - this.boatType = boatType; - this.sourceId = sourceId; - this.hullID = hullID; - this.shortName = shortName; - this.boatName = boatName; - this.country = country; - this.sailIn = false; - this.location = new GeoPoint(57.670341, 11.826856); - this.lastLocation = location; - this.heading = 120.0; //In degrees - this.velocity = 0d; //in mms-1 - - this.hasEnteredRoundingZone = false; - this.hasPassedFirstLine = false; - this.hasPassedSecondLine = false; - } - - /** - * @param timeInterval since last update in milliseconds - */ - public void update(Long timeInterval) { - - Double secondsElapsed = timeInterval / 1000000.0; - Double windSpeedKnots = GameState.getWindSpeedKnots(); - Double trueWindAngle = Math.abs(GameState.getWindDirection() - heading); - Double boatSpeedInKnots = PolarTable.getBoatSpeed(windSpeedKnots, trueWindAngle); - Double maxBoatSpeed = boatSpeedInKnots / 1.943844492 * 1000; - if (sailIn && velocity <= maxBoatSpeed && maxBoatSpeed != 0d) { - - if (velocity < maxBoatSpeed) { - velocity += maxBoatSpeed / 15; // Acceleration - } - if (velocity > maxBoatSpeed) { - velocity = maxBoatSpeed; // Prevent the boats from exceeding top speed - } - - } else { // Deceleration - - if (velocity > 0d) { - if (maxBoatSpeed != 0d) { - velocity -= maxBoatSpeed / 600; - } else { - velocity -= velocity / 100; - } - if (velocity < 0) { - velocity = 0d; - } - } - } - - //UPDATE BOAT LOCATION - location = GeoUtility.getGeoCoordinate(location, heading, velocity * secondsElapsed); - - //CHECK FOR MARK ROUNDING - distanceToNextMark = calcDistanceToNextMark(); - if (distanceToNextMark < ROUNDING_DISTANCE) { - hasEnteredRoundingZone = true; - } - - // TODO: 3/08/17 wmu16 - Implement line cross check here - } - - - /** - * Calculates the distance to the next mark (closest of the two if a gate mark). - * - * @return A distance in metres. Returns -1 if there is no next mark - */ - public Double calcDistanceToNextMark() { - if (nextMark == null) { - return -1d; - } else if (nextMark.isGate()) { - Mark sub1 = nextMark.getSubMark(1); - Mark sub2 = nextMark.getSubMark(2); - Double distance1 = GeoUtility.getDistance(location, sub1); - Double distance2 = GeoUtility.getDistance(location, sub2); - return (distance1 < distance2) ? distance1 : distance2; - } else { - return GeoUtility.getDistance(location, nextMark.getSubMark(1)); - } - } - - public void adjustHeading(Double amount) { - Double newVal = heading + amount; - lastHeading = heading; - heading = (double) Math.floorMod(newVal.longValue(), 360L); - } - - public void tackGybe(Double windDirection) { - Double normalizedHeading = normalizeHeading(); - adjustHeading(-2 * normalizedHeading); - } - - public void toggleSailIn() { - sailIn = !sailIn; - } - - public void turnUpwind() { - Double normalizedHeading = normalizeHeading(); - if (normalizedHeading == 0) { - if (lastHeading < 180) { - adjustHeading(-TURN_STEP); - } else { - adjustHeading(TURN_STEP); - } - } else if (normalizedHeading == 180) { - if (lastHeading < 180) { - adjustHeading(TURN_STEP); - } else { - adjustHeading(-TURN_STEP); - } - } else if (normalizedHeading < 180) { - adjustHeading(-TURN_STEP); - } else { - adjustHeading(TURN_STEP); - } - } - - public void turnDownwind() { - Double normalizedHeading = normalizeHeading(); - if (normalizedHeading == 0) { - if (lastHeading < 180) { - adjustHeading(TURN_STEP); - } else { - adjustHeading(-TURN_STEP); - } - } else if (normalizedHeading == 180) { - if (lastHeading < 180) { - adjustHeading(-TURN_STEP); - } else { - adjustHeading(TURN_STEP); - } - } else if (normalizedHeading < 180) { - adjustHeading(TURN_STEP); - } else { - adjustHeading(-TURN_STEP); - } - } - - 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(); - } 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); - - turnTowardsHeading(optimalHeading); - - } - - private void turnTowardsHeading(Double newHeading) { - System.out.println(newHeading); - if (heading < 90 && newHeading > 270) { - adjustHeading(-TURN_STEP); - } else { - if (heading < newHeading) { - adjustHeading(TURN_STEP); - } else { - adjustHeading(-TURN_STEP); - } - } - } - - private Double normalizeHeading() { - Double normalizedHeading = heading - GameState.windDirection; - normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L); - return normalizedHeading; - } - - public String getBoatType() { - return boatType; - } - - public Integer getSourceId() { - //@TODO Remove and merge with Creating Game Loop - if (sourceId == null) return 0; - return sourceId; - } - - public String getHullID() { - if (hullID == null) return ""; - return hullID; - } - - public String getShortName() { - return shortName; - } - - public String getBoatName() { - return boatName; - } - - public String getCountry() { - if (country == null) return ""; - return country; - } - - public Integer getBoatStatus() { - return boatStatus; - } - - public void setBoatStatus(Integer boatStatus) { - this.boatStatus = boatStatus; - } - - public Integer getLegNumber() { - return legNumber; - } - - public void setLegNumber(Integer legNumber) { -// if (colour != null && position != "-" && legNumber != this.legNumber) { -// RaceViewController.updateYachtPositionSparkline(this, legNumber); -// } - this.legNumber = legNumber; - } - - public void setEstimateTimeTillNextMark(Long estimateTimeTillNextMark) { - timeTillNext = estimateTimeTillNextMark; - } - - public String getEstimateTimeAtFinish() { - DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); - return format.format(estimateTimeAtFinish); - } - - public void setEstimateTimeAtFinish(Long estimateTimeAtFinish) { - this.estimateTimeAtFinish = estimateTimeAtFinish; - } - - public Integer getPositionInteger() { - return positionInt; - } - - public void setPositionInteger(Integer position) { - this.positionInt = position; - } - - public void updateVelocityProperty(double velocity) { - this.velocityProperty.set(velocity); - } - - public void setMarkRoundingTime(Long markRoundingTime) { - this.markRoundTime = markRoundingTime; - } - - public ReadOnlyDoubleProperty getVelocityProperty() { - return velocityProperty.getReadOnlyProperty(); - } - - public double getVelocityMMS() { - return velocity; - } - - public ReadOnlyLongProperty timeTillNextProperty() { - return timeTillNextProperty.getReadOnlyProperty(); - } - - public Double getVelocityKnots() { - return velocity / 1000 * 1.943844492; // TODO: 26/07/17 cir27 - remove magic number - } - - public Long getTimeTillNext() { - return timeTillNext; - } - - public Long getMarkRoundTime() { - return markRoundTime; - } - - public CompoundMark getLastMarkRounded() { - return lastMarkRounded; - } - - public void setLastMarkRounded(CompoundMark lastMarkRounded) { - this.lastMarkRounded = lastMarkRounded; - } - - public void setNextMark(CompoundMark nextMark) { - this.nextMark = nextMark; - } - - public CompoundMark getNextMark(){ - return nextMark; - } - - public GeoPoint getLocation() { - return location; - } - - /** - * Sets the current location of the boat in lat and long whilst preserving the last location - * - * @param lat Latitude - * @param lng Longitude - */ - public void setLocation(Double lat, Double lng) { - lastLocation.setLat(location.getLat()); - lastLocation.setLng(location.getLng()); - location.setLat(lat); - location.setLng(lng); - } - - public Double getHeading() { - return heading; - } - - public void setHeading(Double heading) { - this.heading = heading; - } - - public Boolean getSailIn() { - return sailIn; - } - - @Override - public String toString() { - return boatName; - } - - public void updateTimeSinceLastMarkProperty(long timeSinceLastMark) { - this.timeSinceLastMarkProperty.set(timeSinceLastMark); - } - - public ReadOnlyLongProperty timeSinceLastMarkProperty () { - return timeSinceLastMarkProperty.getReadOnlyProperty(); - } - - public void setTimeTillNext(Long timeTillNext) { - this.timeTillNext = timeTillNext; - } - - - public Color getColour() { - return colour; - } - - public void setColour(Color colour) { - this.colour = colour; - } - - - public Double getVelocity() { - return velocity; - } - - public void setVelocity(Double velocity) { - this.velocity = velocity; - } - - public Double getDistanceToNextMark() { - return distanceToNextMark; - } - - 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); - } - } - - public void addLocationListener (YachtLocationListener listener) { - locationListeners.add(listener); - } -} diff --git a/src/main/java/seng302/model/mark/CompoundMark.java b/src/main/java/seng302/model/mark/CompoundMark.java index af4cd8a8..70cd114f 100644 --- a/src/main/java/seng302/model/mark/CompoundMark.java +++ b/src/main/java/seng302/model/mark/CompoundMark.java @@ -1,27 +1,27 @@ package seng302.model.mark; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +import seng302.gameServer.server.messages.RoundingSide; +import seng302.model.GeoPoint; +import seng302.utilities.GeoUtility; public class CompoundMark { private int compoundMarkId; private String name; - private List marks = new ArrayList<>(); + private GeoPoint midPoint; - public CompoundMark(int markID, String name) { - this.compoundMarkId = markID; - this.name = name; - } - - public void addSubMarks(Mark... marks) { - this.marks.addAll(Arrays.asList(marks)); - } - - public void addSubMarks(List marks) { - this.marks.addAll(marks); + public CompoundMark(int markID, String name, List marks) { + this.compoundMarkId = markID; + this.name = name; + this.marks.addAll(marks); + if (marks.size() > 1) { + this.midPoint = GeoUtility.getDirtyMidPoint(marks.get(0), marks.get(1)); + } else { + this.midPoint = marks.get(0); + } } /** @@ -55,6 +55,27 @@ public class CompoundMark { this.name = name; } + public void setRoundingSide(RoundingSide roundingSide) { + switch (roundingSide) { + case SP: + getSubMark(1).setRoundingSide(RoundingSide.STARBOARD); + getSubMark(2).setRoundingSide(RoundingSide.PORT); + break; + case PS: + getSubMark(1).setRoundingSide(RoundingSide.PORT); + getSubMark(2).setRoundingSide(RoundingSide.STARBOARD); + break; + case PORT: + getSubMark(1).setRoundingSide(RoundingSide.PORT); + break; + case STARBOARD: + getSubMark(1).setRoundingSide(RoundingSide.STARBOARD); + break; + + + } + } + /** * Returns the mark contained in the compound mark. Marks are numbered 1 to n; * @param singleMarkId the id of the desired mark contained in this compound mark. @@ -68,6 +89,16 @@ public class CompoundMark { } } + /** + * NOTE: This is a 'dirty' mid point as it is simply calculated as an xy point would be. + * NO CHECKING FOR LAT / LNG WRAPPING IS DONE IN CREATION OF THIS MIDPOINT + * + * @return GeoPoint of the midpoint of the two marks, or the one mark if there is only one + */ + public GeoPoint getMidPoint() { + return midPoint; + } + /** * Returns whether or not this CompoundMark is a Gate. It is generally cleaner to program to a * specific singleMark or the list of marks. @@ -87,38 +118,6 @@ public class CompoundMark { return marks; } - -// @Override -// public boolean equals(Object other) { -// if (other == null) { -// return false; -// } -// -// if (!(other instanceof Mark)){ -// return false; -// } -// -// Mark otherMark = (Mark) other; -// -// if (otherMark.getLat() != getLat() || otherMark.getLongitude() != getLongitude()) { -// return false; -// } -// -// if (otherMark.getCompoundMarkID() != getCompoundMarkID()){ -// return false; -// } -// -// if (otherMark.getId() != getId()){ -// return false; -// } -// -// if (!otherMark.getName().equals(name)){ -// return false; -// } -// -// return true; -// } - @Override public int hashCode() { int hash = 0; diff --git a/src/main/java/seng302/model/mark/Mark.java b/src/main/java/seng302/model/mark/Mark.java index 57d04974..de66166c 100644 --- a/src/main/java/seng302/model/mark/Mark.java +++ b/src/main/java/seng302/model/mark/Mark.java @@ -2,6 +2,7 @@ package seng302.model.mark; import java.util.ArrayList; import java.util.List; +import seng302.gameServer.server.messages.RoundingSide; import seng302.model.GeoPoint; /** @@ -19,11 +20,13 @@ public class Mark extends GeoPoint { private String name; private int sourceID; private List positionListeners = new ArrayList<>(); + private RoundingSide roundingSide; - public Mark(String name, double lat, double lng, int sourceID) { + public Mark(String name, int seqID, double lat, double lng, int sourceID) { super(lat, lng); this.name = name; this.sourceID = sourceID; + this.seqID = seqID; } /** @@ -39,10 +42,6 @@ public class Mark extends GeoPoint { return seqID; } - public void setSeqID(int seqID) { - this.seqID = seqID; - } - public String getName() { return name; } @@ -55,6 +54,14 @@ public class Mark extends GeoPoint { return sourceID; } + public RoundingSide getRoundingSide() { + return roundingSide; + } + + public void setRoundingSide(RoundingSide roundingSide) { + this.roundingSide = roundingSide; + } + public void setSourceID(int sourceID) { this.sourceID = sourceID; } diff --git a/src/main/java/seng302/model/mark/MarkOrder.java b/src/main/java/seng302/model/mark/MarkOrder.java index ca2b4356..aa75b494 100644 --- a/src/main/java/seng302/model/mark/MarkOrder.java +++ b/src/main/java/seng302/model/mark/MarkOrder.java @@ -1,31 +1,29 @@ 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; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import seng302.gameServer.server.messages.RoundingSide; 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.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Class to hold the order of the marks in the race. */ public class MarkOrder { - private List raceMarkOrder; + private List raceMarkOrder; private Logger logger = LoggerFactory.getLogger(MarkOrder.class); + private Set allMarks; public MarkOrder(){ loadRaceProperties(); @@ -35,7 +33,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; @@ -45,26 +43,38 @@ public class MarkOrder { } /** - * Returns the mark in the race after the previous mark - * @param position The current race position - * @return the next race position - * OR null if there is no position + * @param seqID The seqID of the current mark the boat is heading to + * @return A Boolean indicating if this coming mark is the last one (finish line) */ - public RacePosition getNextPosition(RacePosition position){ - Mark previousMark = position.getNextMark(); - Mark nextMark; + public Boolean isLastMark(Integer seqID) { + return seqID == raceMarkOrder.size() - 1; + } - if (position.getPositionIndex() + 1 >= raceMarkOrder.size() - 1){ - RacePosition nextRacePosition = new RacePosition(raceMarkOrder.size() - 1, null, previousMark); - nextRacePosition.setFinishingLeg(); + /** + * @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 + */ + public CompoundMark getPreviousMark(Integer currentSeqID) throws IndexOutOfBoundsException { + return raceMarkOrder.get(currentSeqID - 1); + } - return nextRacePosition; - } + public CompoundMark getCurrentMark(Integer currentSeqID) { + return raceMarkOrder.get(currentSeqID); + } - Integer nextPositionIndex = position.getPositionIndex() + 1; - RacePosition nextRacePosition = new RacePosition(nextPositionIndex, raceMarkOrder.get(nextPositionIndex), previousMark); + /** + * @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)} + */ + public CompoundMark getNextMark(Integer currentSeqID) throws IndexOutOfBoundsException { + return raceMarkOrder.get(currentSeqID + 1); + } - return nextRacePosition; + public Set getAllMarks(){ + return Collections.unmodifiableSet(allMarks); } /** @@ -72,11 +82,12 @@ 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; Document doc; + allMarks = new HashSet<>(); try { db = dbf.newDocumentBuilder(); @@ -92,11 +103,13 @@ public class MarkOrder { logger.debug("Loaded RaceXML for mark order"); List corners = data.getMarkSequence(); Map marks = data.getCompoundMarks(); - List course = new ArrayList<>(); + List course = new ArrayList<>(); for (Corner corner : corners){ CompoundMark compoundMark = marks.get(corner.getCompoundMarkID()); - course.add(compoundMark.getMarks().get(0)); + compoundMark.setRoundingSide(RoundingSide.getRoundingSide(corner.getRounding())); + course.add(compoundMark); + allMarks.addAll(compoundMark.getMarks()); } return course; @@ -105,17 +118,6 @@ public class MarkOrder { return null; } - /** - * @return The first position in the race - */ - public RacePosition getFirstPosition(){ - if (raceMarkOrder.size() > 0){ - return new RacePosition(-1, raceMarkOrder.get(0), null); - } - - return null; - } - /** * Load the raceXML and mark order */ @@ -132,4 +134,4 @@ public class MarkOrder { } raceMarkOrder = loadRaceOrderFromXML(raceXML); } -} +} \ No newline at end of file diff --git a/src/main/java/seng302/model/mark/RacePosition.java b/src/main/java/seng302/model/mark/RacePosition.java deleted file mode 100644 index fc160b10..00000000 --- a/src/main/java/seng302/model/mark/RacePosition.java +++ /dev/null @@ -1,55 +0,0 @@ -package seng302.model.mark; - -/** - * Represents a boats position between two marks - */ -public class RacePosition { - private Integer positionIndex; - private Mark nextMark; - private Mark previousMark; - private Boolean isFinishingLeg; - - public RacePosition(Integer positionIndex, Mark nextMark, Mark previousMark){ - this.positionIndex = positionIndex; - this.nextMark = nextMark; - this.previousMark = previousMark; - isFinishingLeg = false; - } - - /** - * @return The position of the boat (0...number of marks in race - 1) - */ - public Integer getPositionIndex(){ - return positionIndex; - } - - /** - * @return The mark the boat is heading to - * will return NULL if this is the finishing legg - */ - public Mark getNextMark(){ - return nextMark; - } - - /** - * @return The mark the boat is heading away from, - * Will return NULL if this is the starting leg - */ - public Mark getPreviousMark(){ - return previousMark; - } - - /** - * Sets a flag that this is the last leg in the race - */ - public void setFinishingLeg(){ - isFinishingLeg = true; - } - - /** - * @return true if this is the last leg in the race - */ - public boolean getIsFinishingLeg() { - return isFinishingLeg; - } -} 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/model/stream/parser/YachtEventData.java b/src/main/java/seng302/model/stream/parser/YachtEventData.java new file mode 100644 index 00000000..635bd11f --- /dev/null +++ b/src/main/java/seng302/model/stream/parser/YachtEventData.java @@ -0,0 +1,34 @@ +package seng302.model.stream.parser; + +/** + * Stores parsed data from yacht event code packet + */ +public class YachtEventData { + private Long subjectId; + private Long incidentId; + private Integer eventId; + private Long timeStamp; + + public YachtEventData(Long subjectId, Long incidentId, Integer eventId, Long timeStamp) { + this.subjectId = subjectId; + this.incidentId = incidentId; + this.eventId = eventId; + this.timeStamp = timeStamp; + } + + public Long getSubjectId() { + return subjectId; + } + + public Long getIncidentId() { + return incidentId; + } + + public Integer getEventId() { + return eventId; + } + + public Long getTimeStamp() { + return timeStamp; + } +} diff --git a/src/main/java/seng302/model/stream/xml/generator/Race.java b/src/main/java/seng302/model/stream/xml/generator/Race.java index c9f8cded..cf42cb2c 100644 --- a/src/main/java/seng302/model/stream/xml/generator/Race.java +++ b/src/main/java/seng302/model/stream/xml/generator/Race.java @@ -4,13 +4,14 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import seng302.model.Yacht; +import seng302.model.ServerYacht; /** * A Race object that can be parsed into XML */ public class Race { - private List yachts; + + private List yachts; private LocalDateTime startTime; public Race(){ @@ -22,7 +23,7 @@ public class Race { * Add a boat to the race * @param yacht The boat to add */ - public void addBoat(Yacht yacht){ + public void addBoat(ServerYacht yacht) { yachts.add(yacht); } @@ -30,7 +31,7 @@ public class Race { * Get a list of boats in the race * @return A List of boats */ - public List getBoats(){ + public List getBoats() { return Collections.unmodifiableList(yachts); } diff --git a/src/main/java/seng302/utilities/GeoUtility.java b/src/main/java/seng302/utilities/GeoUtility.java index 9aa61b9d..1c8c2d21 100644 --- a/src/main/java/seng302/utilities/GeoUtility.java +++ b/src/main/java/seng302/utilities/GeoUtility.java @@ -6,6 +6,7 @@ import seng302.model.GeoPoint; public class GeoUtility { private static double EARTH_RADIUS = 6378.137; + private static Double MS_TO_KNOTS = 1.943844492; /** * Calculates the euclidean distance between two markers on the canvas using xy coordinates @@ -45,6 +46,19 @@ public class GeoUtility { return (Math.toDegrees(getBearingRad(p1, p2)) + 360.0) % 360.0; } + + /** + * WARNING: this function DOES NOT account for wrapping around on lats / longs etc. + * SO BE CAREFUL IN USING THIS FUNCTION + * + * @param p1 GeoPoint 1 + * @param p2 GeoPoint 2 + * @return GeoPoint midPoint + */ + public static GeoPoint getDirtyMidPoint(GeoPoint p1, GeoPoint p2) { + return new GeoPoint((p1.getLat() + p2.getLat()) / 2, (p1.getLng() + p2.getLng()) / 2); + } + /** * Calculates the angle between to angular co-ordinates on a sphere in radians. * @@ -93,7 +107,6 @@ public class GeoUtility { return new GeoPoint(Math.toDegrees(endLat), Math.toDegrees(endLng)); } - /** * Performs the line function on two points of a line and a test point to test which side of the * line that point is on. If the return value is return 1, then the point is on one side of the @@ -125,6 +138,31 @@ public class GeoUtility { } } + /** + * Checks if the line formed by lastLocation and location doesn't intersect the line segment + * formed by mark1 and mark2 See the wiki Mark Rounding algorithm for more info + * + * @param mark1 One mark of the line + * @param mark2 The second mark of the line + * @param lastLocation The last location of the point crossing this line + * @param location The current location of the point crossing this line + * @return 0 if two line segment doesn't intersect, otherwise 1 if they intersect and + * lastLocation is on RHS of the line segment (mark1 to mark2) or 2 if lastLocation on LHS of + * the line segment (mark1 to mark2) + */ + public static Integer checkCrossedLine(GeoPoint mark1, GeoPoint mark2, GeoPoint lastLocation, + GeoPoint location) { + boolean enteredDirection = isClockwise(mark1, mark2, lastLocation); + boolean exitedDirection = isClockwise(mark1, mark2, location); + if (enteredDirection != exitedDirection) { + if (!isPointInTriangle(mark1, lastLocation, location, mark2) + && !isPointInTriangle(mark2, lastLocation, location, mark1)) { + + return enteredDirection ? 1 : 2; + } + } + return 0; + } /** * Given a point and a vector (angle and vector length) Will create a new point, that vector @@ -155,10 +193,24 @@ public class GeoUtility { * @param bearing2 the bearing of v2 * @return the difference of bearing from v1 to v2 */ - private static double getBearingDiff(double bearing1, double bearing2) { + private static Double getBearingDiff(double bearing1, double bearing2) { return ((360 - bearing1) + bearing2) % 360; } + /** + * Check if a geo point ins on the right hand side of the line segment, which + * formed by two geo points v1 to v2. (Algorithm: point is clockwise to the + * line if the bearing difference is less than 180 deg.) + * + * @param v1 one end of the line segment + * @param v2 another end of the line segment + * @param point the point to be tested + * @return true if the point is on the RHS of the line + */ + public static Boolean isClockwise(GeoPoint v1, GeoPoint v2, GeoPoint point) { + return getBearingDiff(getBearing(v1, v2), getBearing(v1, point)) < 180; + } + /** * Given three geo points to form a triangle, the method returns true if the fourth point is * inside the triangle @@ -169,18 +221,34 @@ public class GeoUtility { * @param point the point to be tested * @return true if the fourth point is inside the triangle */ - public static boolean isPointInTriangle(GeoPoint v1, GeoPoint v2, GeoPoint v3, GeoPoint point) { - // true, if diff of bearing from (v1->v2) to (v1->p) is less than 180 deg - boolean sideFlag = getBearingDiff(getBearing(v1, v2), getBearing(v1, point)) < 180; + public static Boolean isPointInTriangle(GeoPoint v1, GeoPoint v2, GeoPoint v3, GeoPoint point) { + // true, if diff of bearing from (v1 to v2) to (v1 to p) is less than 180 deg + boolean isCW = isClockwise(v1, v2, point); - if ((getBearingDiff(getBearing(v2, v3), getBearing(v2, point)) < 180) != sideFlag) { + if (isClockwise(v2, v3, point) != isCW) { return false; } - if ((getBearingDiff(getBearing(v3, v1), getBearing(v3, point)) < 180) != sideFlag) { + if (isClockwise(v3, v1, point) != isCW) { return false; } return true; } + + /** + * @param boatSpeedInKnots Speed in knots + * @return The Boat speed in millimeters per second + */ + public static Double knotsToMMS(Double boatSpeedInKnots) { + return boatSpeedInKnots / MS_TO_KNOTS * 1000; + } + + /** + * @param boatSpeedInMMS Speed in millimeters per second + * @return The Boat speed in knots + */ + public static Double mmsToKnots(Double boatSpeedInMMS) { + return boatSpeedInMMS / 1000 * MS_TO_KNOTS; + } } diff --git a/src/main/java/seng302/utilities/StreamParser.java b/src/main/java/seng302/utilities/StreamParser.java index 0f4c48c0..304b105d 100644 --- a/src/main/java/seng302/utilities/StreamParser.java +++ b/src/main/java/seng302/utilities/StreamParser.java @@ -13,15 +13,12 @@ import org.xml.sax.InputSource; import org.xml.sax.SAXException; import seng302.model.stream.packets.PacketType; import seng302.model.stream.packets.StreamPacket; -import seng302.model.stream.parser.MarkRoundingData; -import seng302.model.stream.parser.PositionUpdateData; +import seng302.model.stream.parser.*; import seng302.model.stream.parser.PositionUpdateData.DeviceType; -import seng302.model.stream.parser.RaceStartData; -import seng302.model.stream.parser.RaceStatusData; /** - * StreamParser is a utilities class for taking byte data, formatted according to the AC35 - * streaming protocol, and parsing it into basic data types or collections. + * StreamParser is a utilities class for taking byte data, formatted according to the AC35 streaming + * protocol, and parsing it into basic data types or collections. * * Created by kre39 on 23/04/17. */ @@ -34,8 +31,9 @@ public class StreamParser { * @return the packet sequence number if the packet is of type HEARTBEAT, null otherwise. */ public static Long extractHeartBeat(StreamPacket packet) { - if (packet.getType() != PacketType.HEARTBEAT) + if (packet.getType() != PacketType.HEARTBEAT) { return null; + } long heartbeat = bytesToLong(packet.getPayload()); System.out.println("heartbeat = " + heartbeat); return heartbeat; @@ -52,16 +50,17 @@ public class StreamParser { * containing the parsed packet data. */ public static RaceStatusData extractRaceStatus(StreamPacket packet) { - if (packet.getType() != PacketType.RACE_STATUS) + if (packet.getType() != PacketType.RACE_STATUS) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; long currentTime = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); long raceId = bytesToLong(Arrays.copyOfRange(payload, 7, 11)); int raceStatus = payload[11]; - long expectedStartTime = bytesToLong(Arrays.copyOfRange(payload,12,18)); - long windDir = bytesToLong(Arrays.copyOfRange(payload,18,20)); - long rawWindSpeed = bytesToLong(Arrays.copyOfRange(payload,20,22)); + long expectedStartTime = bytesToLong(Arrays.copyOfRange(payload, 12, 18)); + long windDir = bytesToLong(Arrays.copyOfRange(payload, 18, 20)); + long rawWindSpeed = bytesToLong(Arrays.copyOfRange(payload, 20, 22)); // DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); // currentTime = format.format((new Date(currentTime))) @@ -70,7 +69,6 @@ public class StreamParser { windDir, rawWindSpeed, raceStatus, currentTime, expectedStartTime ); - // long timeTillStart = // ((new Date(expectedStartTime)).getTime() - (new Date(currentTime)).getTime()) / 1000; // @@ -110,7 +108,7 @@ public class StreamParser { // boat.setEstimateTimeAtFinish(estTimeAtFinish); data.addBoatData(boatID, estTimeAtNextMark, estTimeAtFinish, leg, boatStatus); } - return data; + return data; } // private static void setBoatLegPosition(Yacht updatingBoat, Integer leg){ @@ -139,8 +137,9 @@ public class StreamParser { * DISPLAY_TEXT_MESSAGE. */ public static List extractDisplayMessage(StreamPacket packet) { - if (packet.getType() != PacketType.DISPLAY_TEXT_MESSAGE) + if (packet.getType() != PacketType.DISPLAY_TEXT_MESSAGE) { return null; + } List message = new ArrayList<>(); byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; @@ -166,10 +165,11 @@ public class StreamParser { * XML_MESSAGE. */ public static Document extractXmlMessage(StreamPacket packet) { - if ( packet.getType() != PacketType.RACE_XML && - packet.getType() != PacketType.REGATTA_XML && - packet.getType() != PacketType.BOAT_XML ) + if (packet.getType() != PacketType.RACE_XML && + packet.getType() != PacketType.REGATTA_XML && + packet.getType() != PacketType.BOAT_XML) { return null; + } byte[] payload = packet.getPayload(); int messageType = payload[9]; @@ -194,8 +194,8 @@ public class StreamParser { * Extracts the race start status from the packet and returns it as a long array. * * @param packet Packet parsed in to use the payload - * @return An array of form [raceID, raceStartTime, notificationType, timeStamp] or null if - * the packet type is not of RACE_START_STATUS. + * @return An array of form [raceID, raceStartTime, notificationType, timeStamp] or null if the + * packet type is not of RACE_START_STATUS. */ public static RaceStartData extractRaceStartStatus(StreamPacket packet) { if (packet.getType() != PacketType.RACE_START_STATUS) { @@ -212,23 +212,25 @@ public class StreamParser { /** * Parses the the byte array in a StreamPacket for yacht events to retrieve the necessary info - * and returns it a an array of longs. + * and returns it as YachtEventData. * * @param packet Packet parsed in to use the payload - * @return the event data in the form [boatID, incidentID, eventID, timeStamp]. Returns null if - * the packet is not of type YACHT_EVENT_CODE. + * @return the event data in the form of YachtEventData. Returns null if the packet is not of + * type YACHT_EVENT_CODE. */ - public static long[] extractYachtEventCode(StreamPacket packet) { - if (packet.getType() != PacketType.YACHT_EVENT_CODE) + public static YachtEventData extractYachtEventCode(StreamPacket packet) { + if (packet.getType() != PacketType.YACHT_EVENT_CODE) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); + long ackNumber = bytesToLong(Arrays.copyOfRange(payload, 7, 9)); long raceId = bytesToLong(Arrays.copyOfRange(payload, 9, 13)); long subjectId = bytesToLong(Arrays.copyOfRange(payload, 13, 17)); long incidentId = bytesToLong(Arrays.copyOfRange(payload, 17, 21)); int eventId = payload[21]; - return new long[] {subjectId, incidentId, eventId, timeStamp}; + return new YachtEventData(subjectId, incidentId, eventId, timeStamp); } /** @@ -239,15 +241,16 @@ public class StreamParser { * Returns null if the packet is not of type YACHT_ACTION_CODE. */ public static long[] extractYachtActionCode(StreamPacket packet) { - if (packet.getType() != PacketType.YACHT_ACTION_CODE) + if (packet.getType() != PacketType.YACHT_ACTION_CODE) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); long subjectId = bytesToLong(Arrays.copyOfRange(payload, 9, 13)); long incidentId = bytesToLong(Arrays.copyOfRange(payload, 13, 17)); int eventId = payload[17]; - return new long[] {subjectId, incidentId, eventId, timeStamp}; + return new long[]{subjectId, incidentId, eventId, timeStamp}; } /** @@ -258,8 +261,9 @@ public class StreamParser { * CHATTER_TEXT. */ public static String extractChatterText(StreamPacket packet) { - if (packet.getType() != PacketType.CHATTER_TEXT) + if (packet.getType() != PacketType.CHATTER_TEXT) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; int messageType = payload[1]; @@ -276,8 +280,9 @@ public class StreamParser { * is not of type BOAT_LOCATION. */ public static PositionUpdateData extractBoatLocation(StreamPacket packet) { - if (packet.getType() != PacketType.BOAT_LOCATION) + if (packet.getType() != PacketType.BOAT_LOCATION) { return null; + } byte[] payload = packet.getPayload(); int deviceType = (int) payload[15]; long timeValid = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); @@ -293,10 +298,11 @@ public class StreamParser { double groundSpeed = bytesToLong(Arrays.copyOfRange(payload, 38, 40)) / 1000.0; DeviceType type; - if (deviceType == 1) + if (deviceType == 1) { type = DeviceType.YACHT_TYPE; - else + } else { type = DeviceType.MARK_TYPE; + } return new PositionUpdateData((int) boatId, type, lat, lon, heading, groundSpeed); } @@ -309,8 +315,9 @@ public class StreamParser { * if packet is not of type MARK_ROUNDING. */ public static MarkRoundingData extractMarkRounding(StreamPacket packet) { - if (packet.getType() != PacketType.MARK_ROUNDING) + if (packet.getType() != PacketType.MARK_ROUNDING) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); @@ -325,16 +332,17 @@ public class StreamParser { } /** - * Returns a list containing the string value of data within the given stream packet for - * course wind. + * Returns a list containing the string value of data within the given stream packet for course + * wind. * * @param packet The packet containing the payload * @return the string values of the wind packet. Returns null if the packet is not of type * COURSE_WIND. */ public static List extractCourseWind(StreamPacket packet) { - if (packet.getType() != PacketType.COURSE_WIND) + if (packet.getType() != PacketType.COURSE_WIND) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; int selectedWindId = payload[1]; @@ -366,13 +374,13 @@ public class StreamParser { * Returns the parsed data from a StreamPacket for average wind data. * * @param packet The packet containing the payload - * @return The wind data in the form - * [rawPeriod, rawSamplePeriod, period2, speed2, period3, speed3, period4, speed4, timestamp] - * or null if the packet is not of type AVG_WIND. + * @return The wind data in the form [rawPeriod, rawSamplePeriod, period2, speed2, period3, + * speed3, period4, speed4, timestamp] or null if the packet is not of type AVG_WIND. */ public static long[] extractAvgWind(StreamPacket packet) { - if (packet.getType() != PacketType.AVG_WIND) + if (packet.getType() != PacketType.AVG_WIND) { return null; + } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; long timeStamp = bytesToLong(Arrays.copyOfRange(payload, 1, 7)); @@ -384,7 +392,7 @@ public class StreamParser { long speed3 = bytesToLong(Arrays.copyOfRange(payload, 17, 19)); long period4 = bytesToLong(Arrays.copyOfRange(payload, 19, 21)); long speed4 = bytesToLong(Arrays.copyOfRange(payload, 21, 23)); - return new long[] { + return new long[]{ rawPeriod, rawSamplePeriod, period2, speed2, period3, speed3, period4, speed4, timeStamp }; } @@ -410,8 +418,7 @@ public class StreamParser { } /** - * takes an array of up to 7 bytes and returns a positive - * long constructed from the input bytes + * takes an array of up to 7 bytes and returns a positive long constructed from the input bytes * * @param bytes the byte array to conver to Long * @return a positive long if there is less than 7 bytes -1 otherwise diff --git a/src/main/java/seng302/utilities/XMLParser.java b/src/main/java/seng302/utilities/XMLParser.java index c231535d..a08a0769 100644 --- a/src/main/java/seng302/utilities/XMLParser.java +++ b/src/main/java/seng302/utilities/XMLParser.java @@ -8,8 +8,8 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import seng302.model.ClientYacht; import seng302.model.Limit; -import seng302.model.Yacht; import seng302.model.mark.CompoundMark; import seng302.model.mark.Corner; import seng302.model.mark.Mark; @@ -125,8 +125,8 @@ public class XMLParser { * @param doc XML Document Object * @return Mapping of sourceIds to Boats. */ - public static Map parseBoats(Document doc){ - Map competingBoats = new HashMap<>(); + public static Map parseBoats(Document doc) { + Map competingBoats = new HashMap<>(); Element docEle = doc.getDocumentElement(); @@ -135,7 +135,8 @@ public class XMLParser { Node currentBoat = boatsList.item(i); if (currentBoat.getNodeName().equals("Boat")) { // Boat boat = new Boat(currentBoat); - Yacht yacht = new Yacht(XMLParser.getNodeAttributeString(currentBoat, "Type"), + ClientYacht yacht = new ClientYacht( + XMLParser.getNodeAttributeString(currentBoat, "Type"), XMLParser.getNodeAttributeInt(currentBoat, "SourceID"), XMLParser.getNodeAttributeString(currentBoat, "HullNum"), XMLParser.getNodeAttributeString(currentBoat, "ShortName"), @@ -256,9 +257,9 @@ public class XMLParser { if (cMarkNode.getNodeName().equals("CompoundMark")) { cMark = new CompoundMark( XMLParser.getNodeAttributeInt(cMarkNode, "CompoundMarkID"), - XMLParser.getNodeAttributeString(cMarkNode, "Name") + XMLParser.getNodeAttributeString(cMarkNode, "Name"), + createMarks(cMarkNode) ); - cMark.addSubMarks(createMarks(cMarkNode)); allMarks.add(cMark); } } @@ -277,11 +278,12 @@ public class XMLParser { for (int i = 0; i < childMarks.getLength(); i++) { Node markNode = childMarks.item(i); if (markNode.getNodeName().equals("Mark")) { + Integer seqID = XMLParser.getNodeAttributeInt(markNode, "SeqID"); Integer sourceID = XMLParser.getNodeAttributeInt(markNode, "SourceID"); String markName = XMLParser.getNodeAttributeString(markNode, "Name"); Double targetLat = XMLParser.getNodeAttributeDouble(markNode, "TargetLat"); Double targetLng = XMLParser.getNodeAttributeDouble(markNode, "TargetLng"); - Mark mark = new Mark(markName, targetLat, targetLng, sourceID); + Mark mark = new Mark(markName, seqID, targetLat, targetLng, sourceID); subMarks.add(mark); } } 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 915eef37..bd702a47 100644 --- a/src/main/java/seng302/visualiser/GameClient.java +++ b/src/main/java/seng302/visualiser/GameClient.java @@ -13,17 +13,17 @@ 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.ClientYacht; import seng302.model.RaceState; -import seng302.model.Yacht; import seng302.model.stream.packets.StreamPacket; import seng302.model.stream.parser.MarkRoundingData; import seng302.model.stream.parser.PositionUpdateData; import seng302.model.stream.parser.PositionUpdateData.DeviceType; 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; @@ -31,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 { @@ -41,20 +42,27 @@ public class GameClient { private RaceViewController raceView; - private Map allBoatsMap; + private Map allBoatsMap; private RegattaXMLData regattaData; private RaceXMLData courseData; private RaceState raceState = new RaceState(); 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); @@ -62,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); @@ -70,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 { @@ -89,14 +103,14 @@ public class GameClient { loadStartScreen(); } }); + + server.setGameClient(this); } 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( @@ -115,7 +129,8 @@ public class GameClient { * @return the lobby controller. */ private LobbyController loadLobby() { - FXMLLoader fxmlLoader = new FXMLLoader(GameClient.class.getResource("/views/LobbyView.fxml")); + FXMLLoader fxmlLoader = new FXMLLoader( + GameClient.class.getResource("/views/LobbyView.fxml")); try { holderPane.getChildren().clear(); holderPane.getChildren().add(fxmlLoader.load()); @@ -140,10 +155,24 @@ public class GameClient { holderPane.getScene().setOnKeyPressed(this::keyPressed); holderPane.getScene().setOnKeyReleased(this::keyReleased); raceView = fxmlLoader.getController(); - Yacht player = allBoatsMap.get(socketThread.getClientId()); + ClientYacht player = allBoatsMap.get(socketThread.getClientId()); raceView.loadRace(allBoatsMap, courseData, raceState, player); } + private void loadFinishScreenView() { + FXMLLoader fxmlLoader = new FXMLLoader( + getClass().getResource("/views/FinishScreenView.fxml")); + try { + final Node finishScreenFX = fxmlLoader.load(); + Platform.runLater(() -> { + holderPane.getChildren().clear(); + holderPane.getChildren().add(finishScreenFX); + }); + } catch (IOException e) { + e.printStackTrace(); + } + } + private void parsePackets() { while (socketThread.getPacketQueue().peek() != null) { StreamPacket packet = socketThread.getPacketQueue().poll(); @@ -174,17 +203,13 @@ public class GameClient { break; case BOAT_XML: - System.out.println("GOT SUM BOATS YAY :)"); allBoatsMap = XMLParser.parseBoats( 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: @@ -198,13 +223,18 @@ public class GameClient { case MARK_ROUNDING: updateMarkRounding(StreamParser.extractMarkRounding(packet)); break; + + case YACHT_EVENT_CODE: + showCollisionAlert(StreamParser.extractYachtEventCode(packet)); + break; } } } private void startRaceIfAllDataReceived() { - if (allXMLReceived() && raceView == null) + if (allXMLReceived() && raceView == null) { loadRaceView(); + } } private boolean allXMLReceived() { @@ -217,8 +247,8 @@ public class GameClient { private void updatePosition(PositionUpdateData positionData) { if (positionData.getType() == DeviceType.YACHT_TYPE) { if (allXMLReceived() && allBoatsMap.containsKey(positionData.getDeviceId())) { - Yacht yacht = allBoatsMap.get(positionData.getDeviceId()); - yacht.updateLocation(positionData.getLat(), + ClientYacht clientYacht = allBoatsMap.get(positionData.getDeviceId()); + clientYacht.updateLocation(positionData.getLat(), positionData.getLon(), positionData.getHeading(), positionData.getGroundSpeed()); } @@ -235,11 +265,11 @@ public class GameClient { */ private void updateMarkRounding(MarkRoundingData roundingData) { if (allXMLReceived()) { - Yacht yacht = allBoatsMap.get(roundingData.getBoatId()); - yacht.setMarkRoundingTime(roundingData.getTimeStamp()); - yacht.updateTimeSinceLastMarkProperty( + ClientYacht clientYacht = allBoatsMap.get(roundingData.getBoatId()); + clientYacht.setMarkRoundingTime(roundingData.getTimeStamp()); + clientYacht.updateTimeSinceLastMarkProperty( raceState.getRaceTime() - roundingData.getTimeStamp()); - yacht.setLastMarkRounded( + clientYacht.setLastMarkRounded( courseData.getCompoundMarks().get( roundingData.getMarkId() ) @@ -250,21 +280,34 @@ public class GameClient { private void processRaceStatusUpdate(RaceStatusData data) { if (allXMLReceived()) { raceState.updateState(data); + if (raceView != null) { + raceView.getGameView().setWindDir(raceState.getWindDirection()); + } + boolean raceFinished = true; + for (ClientYacht yacht : allBoatsMap.values()) { + if (yacht.getBoatStatus() != 3) { + raceFinished = false; + } + } + if (raceFinished == true) { + loadFinishScreenView(); + } + for (long[] boatData : data.getBoatData()) { - Yacht yacht = allBoatsMap.get((int) boatData[0]); - yacht.setEstimateTimeTillNextMark(raceState.getRaceTime() - boatData[1]); - yacht.setEstimateTimeAtFinish(boatData[2]); + ClientYacht clientYacht = allBoatsMap.get((int) boatData[0]); + clientYacht.setEstimateTimeTillNextMark(raceState.getRaceTime() - boatData[1]); + clientYacht.setEstimateTimeAtFinish(boatData[2]); int legNumber = (int) boatData[3]; - yacht.setLegNumber(legNumber); - yacht.setBoatStatus((int) boatData[4]); - if (legNumber != yacht.getLegNumber()) { + clientYacht.setLegNumber(legNumber); + clientYacht.setBoatStatus((int) boatData[4]); + if (legNumber != clientYacht.getLegNumber()) { int placing = 1; - for (Yacht otherYacht : allBoatsMap.values()) { - if (otherYacht.getSourceId() != boatData[0] && - yacht.getLegNumber() <= otherYacht.getLegNumber()) + for (ClientYacht otherClientYacht : allBoatsMap.values()) { + if (otherClientYacht.getSourceId() != boatData[0] && + clientYacht.getLegNumber() <= otherClientYacht.getLegNumber()) placing++; } - yacht.setPositionInteger(placing); + clientYacht.setPositionInteger(placing); } } } @@ -279,47 +322,50 @@ public class GameClient { * Handle the key-pressed event from the text field. * @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; - } - } - } - - public void keyReleased(KeyEvent e) { + private void keyPressed(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); + 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; } } + + 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 + socketThread.sendBoatAction(BoatAction.SAILS_IN); + raceView.getGameView().getPlayerYacht().toggleSail(); + break; + case PAGE_UP: + case PAGE_DOWN: + socketThread.sendBoatAction(BoatAction.MAINTAIN_HEADING); break; + } + } + + public RaceXMLData getCourseData() { + return courseData; + } + + /** + * Tells race view to show a collision animation. + */ + private void showCollisionAlert(YachtEventData yachtEventData) { + // 33 is the agreed code to show collision + if (yachtEventData.getEventId() == 33) { + raceView.showCollision(yachtEventData.getSubjectId()); + } + } } diff --git a/src/main/java/seng302/visualiser/GameView.java b/src/main/java/seng302/visualiser/GameView.java index e8552533..68710d7f 100644 --- a/src/main/java/seng302/visualiser/GameView.java +++ b/src/main/java/seng302/visualiser/GameView.java @@ -9,6 +9,9 @@ import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import javafx.animation.AnimationTimer; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.geometry.Point2D; @@ -19,12 +22,14 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; +import javafx.scene.shape.Circle; import javafx.scene.shape.Polygon; import javafx.scene.text.Text; +import seng302.model.ClientYacht; +import javafx.util.Duration; import seng302.model.Colors; import seng302.model.GeoPoint; import seng302.model.Limit; -import seng302.model.Yacht; import seng302.model.mark.CompoundMark; import seng302.model.mark.Corner; import seng302.model.mark.Mark; @@ -38,10 +43,10 @@ import seng302.visualiser.map.CanvasMap; */ public class GameView extends Pane { - private double bufferSize = 50; - private double panelWidth = 1260; // it should be 1280 but, minors 40 to cancel the bias. - private double panelHeight = 960; - private double canvasWidth = 1100; + private double bufferSize = 50; + private double panelWidth = 1260; // it should be 1280 but, minors 40 to cancel the bias. + private double panelHeight = 960; + private double canvasWidth = 1100; private double canvasHeight = 920; private boolean horizontalInversion = false; @@ -51,6 +56,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(); @@ -59,8 +66,8 @@ public class GameView extends Pane { private List borderPoints; private Map markerObjects; - private Map boatObjects = new HashMap<>(); - private Map annotations = new HashMap<>(); + private Map boatObjects = new HashMap<>(); + private Map annotations = new HashMap<>(); private ObservableList gameObjects; private Group annotationsGroup = new Group(); private Group wakesGroup = new Group(); @@ -78,13 +85,33 @@ public class GameView extends Pane { private Double frameRate = 60.0; private int frameTimeIndex = 0; private boolean arrayFilled = false; + private ClientYacht 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, VERTICAL } - public GameView () { + public GameView() { gameObjects = this.getChildren(); // create image view for map, bind panel size to image gameObjects.add(mapImage); @@ -97,7 +124,7 @@ public class GameView extends Pane { initializeTimer(); } - private void initializeTimer () { + private void initializeTimer() { Arrays.fill(frameTimes, 1_000_000_000 / 60); timer = new AnimationTimer() { private long lastTime = 0; @@ -133,15 +160,15 @@ public class GameView extends Pane { } } // Platform.runLater(() -> -// boatObjects.forEach((boat, boatObject) -> boatObject.updateLocation()) + boatObjects.forEach((boat, boatObject) -> boatObject.updateLocation()); // ); } }; } /** - * First find the top right and bottom left points' geo locations, then retrieve - * map from google to display on image view. - Haoming 22/5/2017 + * First find the top right and bottom left points' geo locations, then retrieve map from google + * to display on image view. - Haoming 22/5/2017 */ private void drawGoogleMap() { findMetersPerPixel(); @@ -199,13 +226,23 @@ public class GameView extends Pane { for (Mark mark : cMark.getMarks()) { makeAndBindMarker(mark, colour); } + + //UNCOMMENT THIS TO HIGHLIGHT SUBMARKS 1 and 2 RED AND GREEN RESPECTIVELY FOR DEBUG + //(instead of above for loop) +// for (Mark mark : cMark.getMarks()) { +// if (mark.getSeqID() == 1) { +// makeAndBindMarker(mark, Color.RED); +// } else { +// makeAndBindMarker(mark, Color.GREEN); +// } +// } //Create gate line if (cMark.isGate()) { for (int i = 1; i < cMark.getMarks().size(); i++) { gates.add( makeAndBindGate( markerObjects.get(cMark.getSubMark(i)), - markerObjects.get(cMark.getSubMark(i+1)), + markerObjects.get(cMark.getSubMark(i + 1)), colour ) ); @@ -269,7 +306,7 @@ public class GameView extends Pane { gate.endYProperty().bind( m2.layoutYProperty() ); - return gate; + return gate; } /** @@ -307,26 +344,26 @@ public class GameView extends Pane { /** * Draws all the boats. - * @param yachts The yachts to set in the race + * @param clientYachts The yachts to set in the race */ - public void setBoats(List yachts) { + public void setBoats(List clientYachts) { BoatObject newBoat; final List wakes = new ArrayList<>(); - for (Yacht yacht : yachts) { + for (ClientYacht clientYacht : clientYachts) { Paint colour = Colors.getColor(); newBoat = new BoatObject(); newBoat.setFill(colour); - boatObjects.put(yacht, newBoat); - createAndBindAnnotationBox(yacht, colour); + boatObjects.put(clientYacht, newBoat); + createAndBindAnnotationBox(clientYacht, colour); // wakesGroup.getChildren().add(newBoat.getWake()); wakes.add(newBoat.getWake()); 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) ->{ + clientYacht.addLocationListener((boat, lat, lon, heading, sailIn, velocity) -> { 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); @@ -347,11 +384,11 @@ public class GameView extends Pane { }); } - private void createAndBindAnnotationBox (Yacht yacht, Paint colour) { + private void createAndBindAnnotationBox(ClientYacht clientYacht, Paint colour) { AnnotationBox newAnnotation = new AnnotationBox(); newAnnotation.setFill(colour); newAnnotation.addAnnotation( - "name", "Player: " + yacht.getShortName() + "name", "Player: " + clientYacht.getShortName() ); // newAnnotation.addAnnotation( // "velocity", @@ -374,28 +411,28 @@ public class GameView extends Pane { // return format.format(time); // } // ); - annotations.put(yacht, newAnnotation); + annotations.put(clientYacht, newAnnotation); } - private void drawFps(Double fps){ + private void drawFps(Double fps) { Platform.runLater(() -> fpsDisplay.setText(String.format("%d FPS", Math.round(fps)))); } /** - * Sets the class variables minLatPoint, maxLatPoint, minLonPoint, maxLonPoint to the point - * with the leftmost point, rightmost point, southern most point and northern most point + * Sets the class variables minLatPoint, maxLatPoint, minLonPoint, maxLonPoint to the point with + * the leftmost point, rightmost point, southern most point and northern most point * respectively. */ private void findMinMaxPoint(List points) { List sortedPoints = new ArrayList<>(points); sortedPoints.sort(Comparator.comparingDouble(GeoPoint::getLat)); minLatPoint = new GeoPoint(sortedPoints.get(0).getLat(), sortedPoints.get(0).getLng()); - GeoPoint maxLat = sortedPoints.get(sortedPoints.size()-1); + GeoPoint maxLat = sortedPoints.get(sortedPoints.size() - 1); maxLatPoint = new GeoPoint(maxLat.getLat(), maxLat.getLng()); sortedPoints.sort(Comparator.comparingDouble(GeoPoint::getLng)); minLonPoint = new GeoPoint(sortedPoints.get(0).getLat(), sortedPoints.get(0).getLng()); - GeoPoint maxLon = sortedPoints.get(sortedPoints.size()-1); + GeoPoint maxLon = sortedPoints.get(sortedPoints.size() - 1); maxLonPoint = new GeoPoint(maxLon.getLat(), maxLon.getLng()); if (maxLonPoint.getLng() - minLonPoint.getLng() > 180) { horizontalInversion = true; @@ -415,15 +452,19 @@ public class GameView extends Pane { if (scaleDirection == ScaleDirection.HORIZONTAL) { referenceAngle = Math.abs( - GeoUtility.getBearingRad(referencePoint, minLonPoint) + GeoUtility.getBearingRad(referencePoint, minLonPoint) ); - referencePointX = bufferSize + distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility.getDistance(referencePoint, minLonPoint); + referencePointX = + bufferSize + distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility + .getDistance(referencePoint, minLonPoint); referenceAngle = Math.abs(GeoUtility.getDistance(referencePoint, maxLatPoint)); - referencePointY = canvasHeight - (bufferSize + bufferSize); - referencePointY -= distanceScaleFactor * Math.cos(referenceAngle) * GeoUtility.getDistance(referencePoint, maxLatPoint); - referencePointY = referencePointY / 2; + referencePointY = canvasHeight - (bufferSize + bufferSize); + referencePointY -= distanceScaleFactor * Math.cos(referenceAngle) * GeoUtility + .getDistance(referencePoint, maxLatPoint); + referencePointY = referencePointY / 2; referencePointY += bufferSize; - referencePointY += distanceScaleFactor * Math.cos(referenceAngle) * GeoUtility.getDistance(referencePoint, maxLatPoint); + referencePointY += distanceScaleFactor * Math.cos(referenceAngle) * GeoUtility + .getDistance(referencePoint, maxLatPoint); } else { referencePointY = canvasHeight - bufferSize; referenceAngle = Math.abs( @@ -431,11 +472,14 @@ public class GameView extends Pane { GeoUtility.getDistance(referencePoint, minLonPoint) ) ); - referencePointX = bufferSize; - referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility.getDistance(referencePoint, minLonPoint); - referencePointX += ((canvasWidth - (bufferSize + bufferSize)) - (minLonToMaxLon * distanceScaleFactor)) / 2; + referencePointX = bufferSize; + referencePointX += distanceScaleFactor * Math.sin(referenceAngle) * GeoUtility + .getDistance(referencePoint, minLonPoint); + referencePointX += + ((canvasWidth - (bufferSize + bufferSize)) - (minLonToMaxLon * distanceScaleFactor)) + / 2; } - if(horizontalInversion) { + if (horizontalInversion) { referencePointX = canvasWidth - bufferSize - (referencePointX - bufferSize); } } @@ -448,12 +492,12 @@ public class GameView extends Pane { private double scaleRaceExtremities() { double vertAngle = Math.abs( - GeoUtility.getBearingRad(minLatPoint, maxLatPoint) + GeoUtility.getBearingRad(minLatPoint, maxLatPoint) ); double vertDistance = Math.cos(vertAngle) * GeoUtility.getDistance(minLatPoint, maxLatPoint); double horiAngle = Math.abs( - GeoUtility.getBearingRad(minLonPoint, maxLonPoint) + GeoUtility.getBearingRad(minLonPoint, maxLonPoint) ); if (horiAngle <= (Math.PI / 2)) { horiAngle = (Math.PI / 2) - horiAngle; @@ -479,40 +523,45 @@ public class GameView extends Pane { return findScaledXY(unscaled.getLat(), unscaled.getLng()); } - private Point2D findScaledXY (double unscaledLat, double unscaledLon) { + private Point2D findScaledXY(double unscaledLat, double unscaledLon) { double distanceFromReference; double angleFromReference; double xAxisLocation = referencePointX; double yAxisLocation = referencePointY; angleFromReference = GeoUtility.getBearingRad( - minLatPoint, new GeoPoint(unscaledLat, unscaledLon) + minLatPoint, new GeoPoint(unscaledLat, unscaledLon) ); 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); - yAxisLocation -= Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + xAxisLocation += Math + .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + yAxisLocation -= Math + .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); } else if (angleFromReference >= 0) { angleFromReference = angleFromReference - Math.PI / 2; - xAxisLocation += Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); - yAxisLocation += Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + xAxisLocation += Math + .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + yAxisLocation += Math + .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); } else if (angleFromReference < 0 && angleFromReference >= -Math.PI / 2) { angleFromReference = Math.abs(angleFromReference); - xAxisLocation -= Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); - yAxisLocation -= Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + xAxisLocation -= Math + .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + yAxisLocation -= Math + .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); } else { angleFromReference = Math.abs(angleFromReference) - Math.PI / 2; - xAxisLocation -= Math.round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); - yAxisLocation += Math.round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); + xAxisLocation -= Math + .round(distanceScaleFactor * Math.cos(angleFromReference) * distanceFromReference); + yAxisLocation += Math + .round(distanceScaleFactor * Math.sin(angleFromReference) * distanceFromReference); } - if(horizontalInversion) { + if (horizontalInversion) { xAxisLocation = canvasWidth - bufferSize - (xAxisLocation - bufferSize); } -// System.out.println("yAxisLocation = " + yAxisLocation + " " + unscaledLat); -// System.out.println("xAxisLocation = " + xAxisLocation + " " + unscaledLon); return new Point2D(xAxisLocation, yAxisLocation); } @@ -537,7 +586,7 @@ public class GameView extends Pane { metersPerPixelY = dVertical / dy; } - public void setAnnotationVisibilities (boolean teamName, boolean velocity, boolean estTime, + public void setAnnotationVisibilities(boolean teamName, boolean velocity, boolean estTime, boolean legTime, boolean trail, boolean wake) { for (BoatObject boatObject : boatObjects.values()) { boatObject.setVisibility(teamName, velocity, estTime, legTime, trail, wake); @@ -550,25 +599,37 @@ public class GameView extends Pane { } } - public void setFPSVisibility (boolean visibility) { + public void setFPSVisibility(boolean visibility) { fpsDisplay.setVisible(visibility); } - public void selectBoat (Yacht selectedYacht) { + public void selectBoat(ClientYacht selectedClientYacht) { boatObjects.forEach((boat, group) -> - group.setIsSelected(boat == selectedYacht) + group.setIsSelected(boat == selectedClientYacht) ); } - public void pauseRace () { + public void pauseRace() { timer.stop(); } - public void startRace () { + + public void setWindDir(double windDir) { + this.windDir = windDir; + } + + public void startRace() { timer.start(); } - public void setBoatAsPlayer (Yacht playerYacht) { + public ClientYacht getPlayerYacht() { + return playerYacht; + } + + + public void setBoatAsPlayer (ClientYacht playerYacht) { + this.playerYacht = playerYacht; + this.playerYacht.toggleSail(); boatObjects.get(playerYacht).setAsPlayer(); annotations.get(playerYacht).addAnnotation( "velocity", @@ -582,4 +643,40 @@ public class GameView extends Pane { gameObjects.add(annotations.get(playerYacht)); }); } + + /** + * Given yacht geopoint by race view controller, drawCollision will calculate canvas X and Y and + * display a flashing red circle on collision point. + * + * @param collisionPoint yacht collision point + */ + public void drawCollision(GeoPoint collisionPoint) { + Platform.runLater(() -> { + Point2D point = findScaledXY(collisionPoint); + double circleRadius = 0.0; + Circle circle = new Circle(point.getX(), point.getY(), circleRadius, Color.RED); + gameObjects.add(circle); + + circle.setFill(Color.TRANSPARENT); + circle.setStroke(Color.RED); + circle.setStrokeWidth(3); + + Timeline timeline = new Timeline(); + timeline.setCycleCount(1); + + KeyFrame keyframe1 = new KeyFrame(Duration.ZERO, + new KeyValue(circle.radiusProperty(), 0), + new KeyValue(circle.strokeProperty(), Color.TRANSPARENT)); + KeyFrame keyFrame2 = new KeyFrame(new Duration(1000), + new KeyValue(circle.radiusProperty(), 50), + new KeyValue(circle.strokeProperty(), Color.RED)); + KeyFrame keyFrame3 = new KeyFrame(new Duration(1500), + new KeyValue(circle.strokeProperty(), Color.TRANSPARENT)); + + timeline.getKeyFrames().addAll(keyframe1, keyFrame2, keyFrame3); + timeline.play(); + + timeline.setOnFinished(event -> gameObjects.remove(circle)); + }); + } } diff --git a/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java b/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java index db2a5d17..a4dc831b 100644 --- a/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java +++ b/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java @@ -17,24 +17,24 @@ import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; -import seng302.model.Yacht; +import seng302.model.ClientYacht; public class FinishScreenViewController implements Initializable { @FXML private GridPane finishScreenGridPane; @FXML - private TableView finishOrderTable; + private TableView finishOrderTable; @FXML - private TableColumn posCol; + private TableColumn posCol; @FXML - private TableColumn boatNameCol; + private TableColumn boatNameCol; @FXML - private TableColumn shortNameCol; + private TableColumn shortNameCol; @FXML - private TableColumn countryCol; + private TableColumn countryCol; - ObservableList data = FXCollections.observableArrayList(); + ObservableList data = FXCollections.observableArrayList(); @Override public void initialize(URL location, ResourceBundle resources) { @@ -61,9 +61,9 @@ public class FinishScreenViewController implements Initializable { finishOrderTable.refresh(); } - public void setFinishers (List participants) { - List sorted = new ArrayList<>(participants); - sorted.sort(Comparator.comparingInt(Yacht::getPositionInteger)); + public void setFinishers(List participants) { + List sorted = new ArrayList<>(participants); + sorted.sort(Comparator.comparingInt(ClientYacht::getPositionInteger)); finishOrderTable.getItems().setAll(sorted); } 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