diff --git a/pom.xml b/pom.xml index 8d10c6fc..296a4f01 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,13 @@ 2.7.13 test + + + + org.freemarker + freemarker + 2.3.26-incubating + diff --git a/src/main/java/seng302/App.java b/src/main/java/seng302/App.java index e1b8b70b..ed6227bb 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -26,7 +26,7 @@ public class App extends Application { primaryStage.show(); primaryStage.setOnCloseRequest(e -> { - StreamParser.appClose(); +// ClientPacketParser.appClose(); StreamReceiver.noMoreBytes(); System.exit(0); }); diff --git a/src/main/java/seng302/client/ClientState.java b/src/main/java/seng302/client/ClientState.java new file mode 100644 index 00000000..64512a1b --- /dev/null +++ b/src/main/java/seng302/client/ClientState.java @@ -0,0 +1,78 @@ +package seng302.client; + +import com.sun.org.apache.xpath.internal.operations.Bool; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import seng302.models.Yacht; + +/** + * Used by the client to store static variables to be used in game. + */ +public class ClientState { + + private static String hostIp = ""; + private static Boolean isHost = false; + private static Boolean raceStarted = false; + private static Boolean connectedToHost = false; + private static Map boats = new ConcurrentHashMap<>(); + private static Boolean dirtyState = true; + private static String clientSourceId = ""; + + public static String getHostIp() { + return hostIp; + } + + public static void setHostIp(String hostIp) { + ClientState.hostIp = hostIp; + } + + public static Boolean isHost() { + return isHost; + } + + public static void setHost(Boolean isHost) { + ClientState.isHost = isHost; + } + + public static Boolean isRaceStarted() { + return raceStarted; + } + + public static void setRaceStarted(Boolean raceStarted) { + ClientState.raceStarted = raceStarted; + } + + public static Boolean isConnectedToHost() { + return connectedToHost; + } + + public static void setConnectedToHost(Boolean connectedToHost) { + ClientState.connectedToHost = connectedToHost; + } + + public static Map getBoats() { + return boats; + } + + public static Boolean isDirtyState() { + return dirtyState; + } + + public static void setDirtyState(Boolean dirtyState) { + ClientState.dirtyState = dirtyState; + } + + public static String getClientSourceId() { + return clientSourceId; + } + + public static void setClientSourceId(String clientSourceId) { + ClientState.clientSourceId = clientSourceId; + } + + public static void setBoats(Map boats) { + ClientState.boats = boats; + } +} diff --git a/src/main/java/seng302/client/ClientStateQueryingRunnable.java b/src/main/java/seng302/client/ClientStateQueryingRunnable.java new file mode 100644 index 00000000..67cf1dbf --- /dev/null +++ b/src/main/java/seng302/client/ClientStateQueryingRunnable.java @@ -0,0 +1,43 @@ +package seng302.client; + +import java.util.Observable; + +/** + * Used by LobbyController to run a separate thread-loop + * updates the controller when change is detected. + */ +public class ClientStateQueryingRunnable extends Observable implements Runnable { + + private Boolean terminate = false; + + public ClientStateQueryingRunnable() {} + + @Override + public void run() { + while(!terminate) { + // Sleeping the thread so it will respond to the if statement below + // if you know a better fix, pls tell me :) -ryan + try { + Thread.sleep(0); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + if (ClientState.isRaceStarted() && ClientState.isConnectedToHost()) { + setChanged(); + notifyObservers("game started"); + terminate(); + } + + if (ClientState.isDirtyState()) { + setChanged(); + notifyObservers("update players"); + ClientState.setDirtyState(false); + } + } + } + + public void terminate() { + terminate = true; + } +} diff --git a/src/main/java/seng302/controllers/Controller.java b/src/main/java/seng302/controllers/Controller.java new file mode 100644 index 00000000..550f6f81 --- /dev/null +++ b/src/main/java/seng302/controllers/Controller.java @@ -0,0 +1,99 @@ +package seng302.controllers; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Parent; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; +import seng302.client.ClientPacketParser; +import seng302.client.ClientToServerThread; +import seng302.server.messages.BoatActionMessage; +import seng302.server.messages.BoatActionType; + +public class Controller implements Initializable { + + @FXML + private AnchorPane contentPane; + private ClientToServerThread clientToServerThread; + private long lastSendingTime; + private int KEY_STROKE_SENDING_FREQUENCY = 50; + + private Object setContentPane(String jfxUrl) { + try { + contentPane.getChildren().removeAll(); + contentPane.getChildren().clear(); + contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); + FXMLLoader fxmlLoader = new FXMLLoader((getClass().getResource(jfxUrl))); + Parent view = fxmlLoader.load(); + contentPane.getChildren().addAll(view); + return fxmlLoader.getController(); + } catch (javafx.fxml.LoadException e) { + System.err.println(e.getCause()); + } catch (IOException e) { + System.err.println(e); + } + return null; + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); + StartScreenController startScreenController = (StartScreenController) setContentPane("/views/StartScreenView.fxml"); + startScreenController.setController(this); + ClientPacketParser.boatLocations.clear(); + + lastSendingTime = System.currentTimeMillis(); + } + + /** Handle the key-pressed event from the text field. */ + 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); + clientToServerThread.sendBoatActionMessage(boatActionMessage); + break; + case PAGE_UP: // upwind + boatActionMessage = new BoatActionMessage(BoatActionType.UPWIND); + clientToServerThread.sendBoatActionMessage(boatActionMessage); + break; + case PAGE_DOWN: // downwind + boatActionMessage = new BoatActionMessage(BoatActionType.DOWNWIND); + clientToServerThread.sendBoatActionMessage(boatActionMessage); + break; + case ENTER: // tack/gybe + boatActionMessage = new BoatActionMessage(BoatActionType.TACK_GYBE); + clientToServerThread.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) { + 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); + clientToServerThread.sendBoatActionMessage(boatActionMessage); + break; + } + } + + public void setClientToServerThread(ClientToServerThread ctt) { + clientToServerThread = ctt; + } +} diff --git a/src/main/java/seng302/controllers/RaceViewController.java b/src/main/java/seng302/controllers/RaceViewController.java new file mode 100644 index 00000000..436da210 --- /dev/null +++ b/src/main/java/seng302/controllers/RaceViewController.java @@ -0,0 +1,656 @@ +package seng302.controllers; + +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Point2D; +import javafx.scene.Scene; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.chart.XYChart.Series; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Slider; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Line; +import javafx.scene.text.Text; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Duration; +import javafx.util.StringConverter; +import seng302.client.ClientPacketParser; +import seng302.utilities.GeoUtility; +import seng302.controllers.annotations.Annotation; +import seng302.controllers.annotations.ImportantAnnotationController; +import seng302.controllers.annotations.ImportantAnnotationDelegate; +import seng302.controllers.annotations.ImportantAnnotationsState; +import seng302.fxObjects.BoatGroup; +import seng302.fxObjects.MarkGroup; +import seng302.models.*; +import seng302.models.mark.GateMark; +import seng302.models.mark.Mark; +import seng302.models.mark.SingleMark; +import seng302.models.stream.XMLParser; + +import java.io.IOException; +import java.util.*; +import seng302.models.stream.XMLParser.RaceXMLObject.Participant; +import java.util.stream.Collectors; + +/** + * Created by ptg19 on 29/03/17. + */ +public class RaceViewController extends Thread implements ImportantAnnotationDelegate { + + @FXML + private Text windSpeedText; + @FXML + private LineChart raceSparkLine; + @FXML + private NumberAxis sparklineYAxis; + @FXML + private VBox positionVbox; + @FXML + private CheckBox toggleFps; + @FXML + private Text timerLabel; + @FXML + private AnchorPane contentAnchorPane; + @FXML + private Text windArrowText, windDirectionText; + @FXML + private Slider annotationSlider; + @FXML + private Button selectAnnotationBtn; + @FXML + private ComboBox boatSelectionComboBox; + @FXML + private CanvasController includedCanvasController; + + private static ArrayList startingBoats = new ArrayList<>(); + private boolean displayFps; + private Timeline timerTimeline; + private Stage stage; + private static HashMap> sparkLineData = new HashMap<>(); + private static ArrayList racingBoats = new ArrayList<>(); + private ImportantAnnotationsState importantAnnotations; + private Yacht selectedBoat; + + public void initialize() { + // Load a default important annotation state + importantAnnotations = new ImportantAnnotationsState(); + + //Formatting the y axis of the sparkline + raceSparkLine.getYAxis().setRotate(180); + raceSparkLine.getYAxis().setTickLabelRotation(180); + raceSparkLine.getYAxis().setTranslateX(-5); + raceSparkLine.getYAxis().setAutoRanging(false); + sparklineYAxis.setTickMarkVisible(false); + startingBoats = new ArrayList<>(ClientPacketParser.getBoats().values()); + + includedCanvasController.setup(this); + includedCanvasController.initializeCanvas(); + initializeUpdateTimer(); + initialiseFPSCheckBox(); + initialiseAnnotationSlider(); + initialiseBoatSelectionComboBox(); + includedCanvasController.timer.start(); + selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView()); + + } + + + /** + * The important annotations have been changed, update this view + * + * @param importantAnnotationsState The current state of the selected annotations + */ + public void importantAnnotationsChanged(ImportantAnnotationsState importantAnnotationsState) { + this.importantAnnotations = importantAnnotationsState; + setAnnotations((int) annotationSlider.getValue()); // Refresh the displayed annotations + } + + + /** + * Loads the "select annotations" view in a new window + */ + private void loadSelectAnnotationView() { + try { + FXMLLoader fxmlLoader = new FXMLLoader(); + Stage stage = new Stage(); + + // Set controller + ImportantAnnotationController controller = new ImportantAnnotationController(this, + stage); + fxmlLoader.setController(controller); + + // Load FXML and set CSS + fxmlLoader + .setLocation(getClass().getResource("/views/importantAnnotationSelectView.fxml")); + Scene scene = new Scene(fxmlLoader.load(), 469, 298); + scene.getStylesheets().add(getClass().getResource("/css/master.css").toString()); + stage.initStyle(StageStyle.UNDECORATED); + stage.setScene(scene); + stage.show(); + + controller.loadState(importantAnnotations); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + + private void initialiseFPSCheckBox() { + displayFps = true; + toggleFps.selectedProperty().addListener( + (observable, oldValue, newValue) -> displayFps = !displayFps); + } + + private void initialiseAnnotationSlider() { + annotationSlider.setLabelFormatter(new StringConverter() { + @Override + public String toString(Double n) { + if (n == 0) { + return "None"; + } + if (n == 1) { + return "Important"; + } + if (n == 2) { + return "All"; + } + + return "All"; + } + + @Override + public Double fromString(String s) { + switch (s) { + case "None": + return 0d; + case "Important": + return 1d; + case "All": + return 2d; + + default: + return 2d; + } + } + }); + + annotationSlider.valueProperty().addListener((obs, oldval, newVal) -> + setAnnotations((int) annotationSlider.getValue())); + + annotationSlider.setValue(2); + } + + + /** + * Used to add any new boats into the race that may have started late or not have had data received yet + */ + void updateSparkLine(){ + // Collect the racing boats that aren't already in the chart + ArrayList sparkLineCandidates = startingBoats.stream().filter(yacht -> !sparkLineData.containsKey(yacht.getSourceId()) + && yacht.getPosition() != null & yacht.getPosition() != "-").collect(Collectors.toCollection(ArrayList::new)); + + // Obtain the qualifying boats to set the max on the Y axis + racingBoats = startingBoats.stream().filter(yacht -> + yacht.getPosition() != null & yacht.getPosition() != "-").collect(Collectors.toCollection(ArrayList::new)); + sparklineYAxis.setUpperBound(racingBoats.size() + 1); + + // Create a new data series for new boats + sparkLineCandidates.stream().filter(yacht -> yacht.getPosition() != null).forEach(yacht -> { + Series yachtData = new Series<>(); + yachtData.setName(yacht.getBoatName()); + yachtData.getData().add(new XYChart.Data<>(Integer.toString(yacht.getLegNumber()), 1 + racingBoats.size() - Double.parseDouble(yacht.getPosition()))); + sparkLineData.put(yacht.getSourceId(), yachtData); + }); + + // Lambda function to sort the series in order of leg (later legs shown more to the right) + List> positions = new ArrayList<>(sparkLineData.values()); + Collections.sort(positions, (o1, o2) -> { + Integer leg1 = Integer.parseInt(o1.getData().get(o1.getData().size()-1).getXValue()); + Integer leg2 = Integer.parseInt(o2.getData().get(o2.getData().size()-1).getXValue()); + if (leg2 < leg1){ + return 1; + } else { + return -1; + } + }); + + // Adds the new data series to the sparkline (and set the colour of the series) + raceSparkLine.setCreateSymbols(false); + positions.stream().filter(spark -> !raceSparkLine.getData().contains(spark)).forEach(spark -> { + raceSparkLine.getData().add(spark); + spark.getNode().lookup(".chart-series-line").setStyle("-fx-stroke:" + getBoatColorAsRGB(spark.getName())); + }); + } + + + /** + * Updates the yachts sparkline of the desired boat and using the new leg number + * @param yacht The yacht to be updated on the sparkline + * @param legNumber the leg number that the position will be assigned to + */ + public static void updateYachtPositionSparkline(Yacht yacht, Integer legNumber){ + XYChart.Series positionData = sparkLineData.get(yacht.getSourceId()); + positionData.getData().add(new XYChart.Data<>(Integer.toString(legNumber), 1 + racingBoats.size() - Double.parseDouble(yacht.getPosition()))); + } + + + /** + * gets the rgb string of the boats colour to use for the chart via css + * @param boatName boat passed in to get the boats colour + * @return the colour as an rgb string + */ + private String getBoatColorAsRGB(String boatName){ + Color color = Color.WHITE; + for (Yacht yacht: startingBoats){ + if (Objects.equals(yacht.getBoatName(), boatName)){ + color = yacht.getColour(); + } + } + if (color == null){ + return String.format( "#%02X%02X%02X",255,255,255); + } + return String.format( "#%02X%02X%02X", + (int)( color.getRed() * 255 ), + (int)( color.getGreen() * 255 ), + (int)( color.getBlue() * 255 ) ); + } + + + /** + * Initalises a timer which updates elements of the RaceView such as wind direction, boat + * orderings etc.. which are dependent on the info from the stream parser constantly. + * Updates of each of these attributes are called ONCE EACH SECOND + */ + private void initializeUpdateTimer() { + timerTimeline = new Timeline(); + timerTimeline.setCycleCount(Timeline.INDEFINITE); + // Run timer update every second + timerTimeline.getKeyFrames().add( + new KeyFrame(Duration.seconds(1), + event -> { + updateRaceTime(); + updateWindDirection(); +// updateOrder(); + updateBoatSelectionComboBox(); + }) + ); + + // Start the timer + timerTimeline.playFromStart(); + } + + + /** + * Iterates over all corners until ones SeqID matches with the boats current leg number. + * Then it gets the compoundMarkID of that corner and uses it to fetch the appropriate mark + * Returns null if no next mark found. + * @param bg The BoatGroup to find the next mark of + * @return The next Mark or null if none found + */ + private Mark getNextMark(BoatGroup bg) { + Integer legNumber = bg.getBoat().getLegNumber(); + + List markSequence = ClientPacketParser.getXmlObject() + .getRaceXML().getCompoundMarkSequence(); + + if (legNumber == 0) { + return null; + } else if (legNumber == markSequence.size() - 1) { + return null; + } + + for (XMLParser.RaceXMLObject.Corner corner : markSequence) { + if (legNumber + 2 == corner.getSeqID()) { + Integer thisCompoundMarkID = corner.getCompoundMarkID(); + + for (Mark mark : ClientPacketParser.getXmlObject().getRaceXML() + .getAllCompoundMarks()) { + if (mark.getCompoundMarkID() == thisCompoundMarkID) { + return mark; + } + } + } + } + + return null; + } + + + /** + * Updates the wind direction arrow and text as from info from the ClientPacketParser + */ + private void updateWindDirection() { + windDirectionText.setText(String.format("%.1f°", ClientPacketParser.getWindDirection())); + windArrowText.setRotate(ClientPacketParser.getWindDirection()); + windSpeedText.setText(String.format("%.1f Knots", ClientPacketParser.getWindSpeed())); + } + + + /** + * Updates the clock for the race + */ + private void updateRaceTime() { + if (ClientPacketParser.isRaceFinished()) { + timerLabel.setFill(Color.RED); + timerLabel.setText("Race Finished!"); + } else { + timerLabel.setText(getTimeSinceStartOfRace()); + } + } + + + /** + * Grabs the boats currently in the race as from the ClientPacketParser and sets them to be selectable + * in the boat selection combo box + */ + private void updateBoatSelectionComboBox() { + ObservableList observableBoats = FXCollections + .observableArrayList(ClientPacketParser.getBoatsPos().values()); + boatSelectionComboBox.setItems(observableBoats); + } + + + /** + * Updates the order of the boats as from the ClientPacketParser and sets them in the boat order + * section + */ + private void updateOrder() { + positionVbox.getChildren().clear(); + positionVbox.getChildren().removeAll(); + positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString()); + + // list of racing boat id + ArrayList participants = ClientPacketParser.getXmlObject().getRaceXML() + .getParticipants(); + ArrayList participantIDs = new ArrayList<>(); + for (Participant p : participants) { + participantIDs.add(p.getsourceID()); + } + + if (ClientPacketParser.isRaceStarted()) { + for (Yacht boat : ClientPacketParser.getBoatsPos().values()) { + if (participantIDs.contains(boat.getSourceId())) { // check if the boat is racing + if (boat.getBoatStatus() == 3) { // 3 is finish status + Text textToAdd = new Text(boat.getPosition() + ". " + + boat.getShortName() + " (Finished)"); + textToAdd.setFill(Paint.valueOf("#d3d3d3")); + positionVbox.getChildren().add(textToAdd); + + } else { + Text textToAdd = new Text(boat.getPosition() + ". " + + boat.getShortName() + " "); + textToAdd.setFill(Paint.valueOf("#d3d3d3")); + textToAdd.setStyle(""); + positionVbox.getChildren().add(textToAdd); + } + } + } + } else { + for (Yacht boat : ClientPacketParser.getBoats().values()) { + if (participantIDs.contains(boat.getSourceId())) { // check if the boat is racing + Text textToAdd = new Text(boat.getPosition() + ". " + + boat.getShortName() + " "); + textToAdd.setFill(Paint.valueOf("#d3d3d3")); + textToAdd.setStyle(""); + positionVbox.getChildren().add(textToAdd); + } + } + } + } + + + private void updateLaylines(BoatGroup bg) { + + Mark nextMark = getNextMark(bg); + Boolean isUpwind = null; + // Can only calc leg direction if there is a next mark and it is a gate mark + if (nextMark != null) { + if (nextMark instanceof GateMark) { + if (bg.isUpwindLeg(includedCanvasController, nextMark)) { + isUpwind = true; + } else { + isUpwind = false; + } + + for(MarkGroup mg : includedCanvasController.getMarkGroups()) { + + mg.removeLaylines(); + + if (mg.getMainMark().getId() == nextMark.getId()) { + + SingleMark singleMark1 = ((GateMark) nextMark).getSingleMark1(); + SingleMark singleMark2 = ((GateMark) nextMark).getSingleMark2(); + Point2D markPoint1 = includedCanvasController.findScaledXY(singleMark1.getLatitude(), singleMark1.getLongitude()); + Point2D markPoint2 = includedCanvasController.findScaledXY(singleMark2.getLatitude(), singleMark2.getLongitude()); + HashMap angleAndSpeed; + if (isUpwind) { + angleAndSpeed = PolarTable + .getOptimalUpwindVMG(ClientPacketParser.getWindSpeed()); + } else { + angleAndSpeed = PolarTable + .getOptimalDownwindVMG(ClientPacketParser.getWindSpeed()); + } + + Double resultingAngle = angleAndSpeed.keySet().iterator().next(); + + + Point2D boatCurrentPos = new Point2D(bg.getBoatLayoutX(), bg.getBoatLayoutY()); + Point2D gateMidPoint = markPoint1.midpoint(markPoint2); + Integer lineFuncResult = GeoUtility.lineFunction(boatCurrentPos, gateMidPoint, markPoint2); + Line rightLayline = new Line(); + Line leftLayline = new Line(); + if (lineFuncResult == 1) { + rightLayline = makeRightLayline(markPoint2, 180 - resultingAngle, + ClientPacketParser + .getWindDirection()); + leftLayline = makeLeftLayline(markPoint1, 180 - resultingAngle, + ClientPacketParser + .getWindDirection()); + } else if (lineFuncResult == -1) { + rightLayline = makeRightLayline(markPoint1, 180 - resultingAngle, + ClientPacketParser + .getWindDirection()); + leftLayline = makeLeftLayline(markPoint2, 180 - resultingAngle, + ClientPacketParser + .getWindDirection()); + } + + leftLayline.setStrokeWidth(0.5); + leftLayline.setStroke(bg.getBoat().getColour()); + + rightLayline.setStrokeWidth(0.5); + rightLayline.setStroke(bg.getBoat().getColour()); + + bg.setLaylines(leftLayline, rightLayline); + mg.addLaylines(leftLayline, rightLayline); + + } + } + } + } + } + + + private Point2D getPointRotation(Point2D ref, Double distance, Double angle){ + Double newX = ref.getX() + (ref.getX() + distance -ref.getX())*Math.cos(angle) - (ref.getY() + distance -ref.getY())*Math.sin(angle); + Double newY = ref.getY() + (ref.getX() + distance -ref.getX())*Math.sin(angle) + (ref.getY() + distance -ref.getY())*Math.cos(angle); + + return new Point2D(newX, newY); + } + + + public Line makeLeftLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) { + + Point2D ep = getPointRotation(startPoint, 50.0, baseAngle + layLineAngle); + Line line = new Line(startPoint.getX(), startPoint.getY(), ep.getX(), ep.getY()); + return line; + + } + + + public Line makeRightLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) { + + Point2D ep = getPointRotation(startPoint, 50.0, baseAngle - layLineAngle); + Line line = new Line(startPoint.getX(), startPoint.getY(), ep.getX(), ep.getY()); + return line; + + } + + + /** + * Initialised the combo box with any boats currently in the race and adds the required listener + * for the combobox to take action upon selection + */ + private void initialiseBoatSelectionComboBox() { + updateBoatSelectionComboBox(); + boatSelectionComboBox.valueProperty().addListener((observable, oldValue, newValue) -> { + //This listener is fired whenever the combo box changes. This means when the values are updated + //We dont want to set the selected value if the values are updated but nothing clicked (null) + if (newValue != null && newValue != selectedBoat) { + Yacht thisYacht = (Yacht) newValue; + setSelectedBoat(thisYacht); + } + }); + } + + + /** + * Display the list of boats in the order they finished the race + */ + private void loadRaceResultView() { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/views/FinishView.fxml")); + + try { + contentAnchorPane.getChildren().removeAll(); + contentAnchorPane.getChildren().clear(); + contentAnchorPane.getChildren().addAll((Pane) loader.load()); + + } catch (javafx.fxml.LoadException e) { + System.err.println(e.getCause()); + } catch (IOException e) { + System.err.println(e); + } + } + + + /** + * Convert seconds to a string of the format mm:ss + * + * @param time the time in seconds + * @return a formatted string + */ + public String convertTimeToMinutesSeconds(int time) { + if (time < 0) { + return String.format("-%02d:%02d", (time * -1) / 60, (time * -1) % 60); + } + return String.format("%02d:%02d", time / 60, time % 60); + } + + private String getTimeSinceStartOfRace() { + String timerString = "0:00"; + if (ClientPacketParser.getTimeSinceStart() > 0) { + String timerMinute = Long.toString(ClientPacketParser.getTimeSinceStart() / 60); + String timerSecond = Long.toString(ClientPacketParser.getTimeSinceStart() % 60); + if (timerSecond.length() == 1) { + timerSecond = "0" + timerSecond; + } + timerString = "-" + timerMinute + ":" + timerSecond; + } else { + String timerMinute = Long.toString(-1 * ClientPacketParser.getTimeSinceStart() / 60); + String timerSecond = Long.toString(-1 * ClientPacketParser.getTimeSinceStart() % 60); + if (timerSecond.length() == 1) { + timerSecond = "0" + timerSecond; + } + timerString = timerMinute + ":" + timerSecond; + } + return timerString; + } + + + boolean isDisplayFps() { + return displayFps; + } + + private void setAnnotations(Integer annotationLevel) { + switch (annotationLevel) { + // No Annotations + case 0: + for (BoatGroup bg : includedCanvasController.getBoatGroups()) { + bg.setVisibility(false, false, false, false, false, false); + } + break; + // Important Annotations + case 1: + for (BoatGroup bg : includedCanvasController.getBoatGroups()) { + bg.setVisibility( + importantAnnotations.getAnnotationState(Annotation.NAME), + importantAnnotations.getAnnotationState(Annotation.SPEED), + importantAnnotations.getAnnotationState(Annotation.ESTTIMETONEXTMARK), + importantAnnotations.getAnnotationState(Annotation.LEGTIME), + importantAnnotations.getAnnotationState(Annotation.TRACK), + importantAnnotations.getAnnotationState(Annotation.WAKE) + ); + } + break; + // All Annotations + case 2: + for (BoatGroup bg : includedCanvasController.getBoatGroups()) { + bg.setVisibility(true, true, true, true, true, true); + } + break; + } + } + + + /** + * Sets all the annotations of the selected boat to be visible and all others to be hidden + * + * @param yacht The yacht for which we want to view all annotations + */ + private void setSelectedBoat(Yacht yacht) { + for (BoatGroup bg : includedCanvasController.getBoatGroups()) { + //We need to iterate over all race groups to get the matching boat group belonging to this boat if we + //are to toggle its annotations, there is no other backwards knowledge of a yacht to its boatgroup. + if (bg.getBoat().getHullID().equals(yacht.getHullID())) { + updateLaylines(bg); + bg.setIsSelected(true); + selectedBoat = yacht; + } else { + bg.setIsSelected(false); + } + } + } + + void setStage(Stage stage) { + this.stage = stage; + } + + Stage getStage() { + return stage; + } + + /** + * Used for when the boat attempts to add data to the sparkline (first checks if the sparkline contains info on it) + * @param yachtId + * @return + */ + public static boolean sparkLineStatus(Integer yachtId) { + return sparkLineData.containsKey(yachtId); + } + +} \ No newline at end of file diff --git a/src/main/java/seng302/gameServer/GameState.java b/src/main/java/seng302/gameServer/GameState.java index fcae2838..f7c95a6a 100644 --- a/src/main/java/seng302/gameServer/GameState.java +++ b/src/main/java/seng302/gameServer/GameState.java @@ -1,8 +1,12 @@ package seng302.gameServer; +import java.util.*; + +import seng302.models.Player; import seng302.model.Player; -import java.util.ArrayList; +import seng302.models.Yacht; +import seng302.server.messages.BoatActionType; /** * A Static class to hold information about the current state of the game (model) @@ -10,23 +14,38 @@ import java.util.ArrayList; */ public class GameState { + private static Long previousUpdateTime; + public static Double windDirection; + private static Double windSpeed; + private static String hostIpAddress; - private static ArrayList players; + private static List players; + private static Map yachts; private static Boolean isRaceStarted; private static GameStages currentStage; public GameState(String hostIpAddress) { + windDirection = 170d; + windSpeed = 10000d; + 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<>(); } public static String getHostIpAddress() { return hostIpAddress; } - public static ArrayList getPlayers() { + public static List getPlayers() { return players; } @@ -38,6 +57,14 @@ public class GameState { players.remove(player); } + public static void addYacht(Integer sourceId, Yacht yacht) { + yachts.put(sourceId, yacht); + } + + public static void removeYacht(Integer yachtId) { + yachts.remove(yachtId); + } + public static Boolean getIsRaceStarted() { return isRaceStarted; } @@ -50,16 +77,72 @@ public class GameState { GameState.currentStage = currentStage; } - /** - * This iterates through all players and updates each players info to its new state based on its current data - */ - private void update(){ - for(Player player : players) { - // TODO: 10/07/17 wmu16 - Update all player info + public static Double getWindDirection() { + return windDirection; + } + + public static Double getWindSpeed() { + return windSpeed; + } + + 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: +// System.out.println("Snapping to VMG"); + // TODO: 22/07/17 wmu16 - Add in the vmg calculation code here + 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; + } + +// System.out.println("-----------------------"); +// System.out.println("Heading: " + playerYacht.getHeading()); +// System.out.println("Sails are in: " + playerYacht.getSailIn()); +// System.out.println("Lat: " + playerYacht.getLocation().getLat()); +// System.out.println("Lng: " + playerYacht.getLocation().getLng()); +// System.out.println("-----------------------\n"); + } + + public static 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() { + // TODO: 22/07/17 wmu16 - This may not be robust enough and may have to be improved on. + return yachts.size() + 1; + } } diff --git a/src/main/java/seng302/gameServer/HeartbeatThread.java b/src/main/java/seng302/gameServer/HeartbeatThread.java index 2057b1bd..415d1e57 100644 --- a/src/main/java/seng302/gameServer/HeartbeatThread.java +++ b/src/main/java/seng302/gameServer/HeartbeatThread.java @@ -13,7 +13,7 @@ import java.util.*; * cannot be sent to a player */ public class HeartbeatThread extends Thread{ - private final int HEARTBEAT_PERIOD = 5000; + private final int HEARTBEAT_PERIOD = 200; private ClientConnectionDelegate delegate; private Integer seqNum; private Stack disconnectedPlayers; @@ -43,7 +43,7 @@ public class HeartbeatThread extends Thread{ Message heartbeat = new Heartbeat(seqNum); for (Player player : GameState.getPlayers()){ - if (!player.getSocket().isConnected()){ + if (!player.getSocket().isConnected()) { playerLostConnection(player); } diff --git a/src/main/java/seng302/gameServer/MainServerThread.java b/src/main/java/seng302/gameServer/MainServerThread.java index ebcdc51c..d2cc3473 100644 --- a/src/main/java/seng302/gameServer/MainServerThread.java +++ b/src/main/java/seng302/gameServer/MainServerThread.java @@ -1,9 +1,11 @@ package seng302.gameServer; -import seng302.model.Player; -import seng302.model.stream.PacketBufferDelegate; -import seng302.model.stream.parsers.StreamParser; -import seng302.model.stream.packets.StreamPacket; +import java.time.LocalDateTime; +import java.util.Observable; +import seng302.client.ClientPacketParser; +import seng302.models.Player; +import seng302.models.stream.PacketBufferDelegate; +import seng302.models.stream.packets.StreamPacket; import java.io.IOException; import java.net.ServerSocket; @@ -15,12 +17,15 @@ import java.util.concurrent.PriorityBlockingQueue; * 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 Thread implements PacketBufferDelegate, ClientConnectionDelegate{ +public class MainServerThread extends Observable implements Runnable, PacketBufferDelegate, ClientConnectionDelegate{ - private static final int PORT = 4950; + private static final int PORT = 4942; private static final Integer MAX_NUM_PLAYERS = 3; + private static final Integer UPDATES_PER_SECOND = 2; private static final int LOG_LEVEL = 1; + private Thread thread; + private ServerSocket serverSocket = null; private Socket socket; private ArrayList serverToClientThreads = new ArrayList<>(); @@ -32,10 +37,13 @@ public class MainServerThread extends Thread implements PacketBufferDelegate, Cl try { serverSocket = new ServerSocket(PORT); } catch (IOException e) { - System.out.println("IO error in server thread handler upon trying to make new server socket"); + serverLog("IO error in server thread handler upon trying to make new server socket", 0); } packetBuffer = new PriorityBlockingQueue<>(); + + thread = new Thread(this); + thread.start(); } @@ -49,43 +57,40 @@ public class MainServerThread extends Thread implements PacketBufferDelegate, Cl heartbeatThread.start(); serverListenThread.start(); + //You should handle interrupts in some way, so that the thread won't keep on forever if you exit the app. - while (!isInterrupted()) { + while (!thread.isInterrupted()) { try { - Thread.sleep(1000 / 60); //60 times per second we should calculate the game state + Thread.sleep(1000 / UPDATES_PER_SECOND); } catch (InterruptedException e) { e.printStackTrace(); } - + if (GameState.getCurrentStage() == GameStages.PRE_RACE) { + GameState.update(); + } //RACING if (GameState.getCurrentStage() == GameStages.RACING) { + GameState.update(); updateClients(); } - //FINISHED else if (GameState.getCurrentStage() == GameStages.FINISHED) { } - updateClients(); - while (!packetBuffer.isEmpty()){ - System.out.println("WHATUPPP"); try { StreamPacket packet = packetBuffer.take(); - StreamParser.parsePacket(packet); + ClientPacketParser.parsePacket(packet); } catch (InterruptedException e) { continue; } } } - System.out.println("WHOOPSIES"); - - // TODO: 14/07/17 wmu16 - Send out disconnect packet to clients try { serverSocket.close(); @@ -95,7 +100,6 @@ public class MainServerThread extends Thread implements PacketBufferDelegate, Cl } } - public void updateClients() { for (ServerToClientThread serverToClientThread : serverToClientThreads) { serverToClientThread.updateClient(); @@ -105,32 +109,26 @@ public class MainServerThread extends Thread implements PacketBufferDelegate, Cl static void serverLog(String message, int logLevel){ if(logLevel <= LOG_LEVEL){ - System.out.println("[SERVER] " + message); + System.out.println("[SERVER " + LocalDateTime.now().toLocalTime().toString() + "] " + message); } } @Override public boolean addToBuffer(StreamPacket streamPacket) { - System.out.println("HEY HI"); return packetBuffer.add(streamPacket); } - private void initializeRace(){ - for (ServerToClientThread serverToClientThread : serverToClientThreads) { - serverToClientThread.updateClient(); - } - } - - /** * 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.getName(), 0); + serverLog("Player Connected From " + serverToClientThread.getThread().getName(), 0); serverToClientThreads.add(serverToClientThread); - + this.addObserver(serverToClientThread); + setChanged(); + notifyObservers(); } /** @@ -139,9 +137,26 @@ public class MainServerThread extends Thread implements PacketBufferDelegate, Cl */ @Override public void clientDisconnected(Player player) { - serverLog("Player disconnected", 0); + try { + player.getSocket().close(); + } catch (Exception e) { + serverLog("Cannot disconnect the socket for the disconnected player.", 0); + } + serverLog("Player " + player.getYacht().getSourceId() + "'s socket disconnected", 0); + GameState.removeYacht(player.getYacht().getSourceId()); GameState.removePlayer(player); -// sendXml(); + for (ServerToClientThread serverToClientThread : serverToClientThreads) { + if (serverToClientThread.getSocket() == player.getSocket()) { + this.deleteObserver(serverToClientThread); + } + } + setChanged(); + notifyObservers(); } + public void startGame() { + for (ServerToClientThread serverToClientThread : serverToClientThreads) { + serverToClientThread.sendRaceStatusMessage(); + } + } } diff --git a/src/main/java/seng302/gameServer/ServerListenThread.java b/src/main/java/seng302/gameServer/ServerListenThread.java index 123e1c53..1313dc6c 100644 --- a/src/main/java/seng302/gameServer/ServerListenThread.java +++ b/src/main/java/seng302/gameServer/ServerListenThread.java @@ -25,7 +25,6 @@ public class ServerListenThread extends Thread{ Socket thisClient = serverSocket.accept(); if (thisClient != null){ ServerToClientThread thisConnection = new ServerToClientThread(thisClient); - thisConnection.start(); delegate.clientConnected(thisConnection); } } catch (IOException e) { diff --git a/src/main/java/seng302/gameServer/ServerPacketParser.java b/src/main/java/seng302/gameServer/ServerPacketParser.java new file mode 100644 index 00000000..155ebe04 --- /dev/null +++ b/src/main/java/seng302/gameServer/ServerPacketParser.java @@ -0,0 +1,37 @@ +package seng302.gameServer; + +import java.util.Arrays; +import seng302.models.stream.packets.StreamPacket; +import seng302.server.messages.BoatActionType; + + +public class ServerPacketParser { + + + public static BoatActionType extractBoatAction(StreamPacket packet) { + byte[] payload = packet.getPayload(); + int messageVersionNo = payload[0]; + long actionTypeValue = bytesToLong(Arrays.copyOfRange(payload, 0, 1)); + return BoatActionType.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; + } +} + diff --git a/src/main/java/seng302/gameServer/ServerToClientThread.java b/src/main/java/seng302/gameServer/ServerToClientThread.java index 68306595..0fd3768f 100644 --- a/src/main/java/seng302/gameServer/ServerToClientThread.java +++ b/src/main/java/seng302/gameServer/ServerToClientThread.java @@ -1,34 +1,74 @@ package seng302.gameServer; -import seng302.model.Player; -import seng302.model.stream.parsers.StreamParser; -import seng302.model.stream.packets.StreamPacket; -import seng302.server.messages.ChatterMessage; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +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; +import java.util.Observer; +import java.util.Random; +import java.util.zip.CRC32; +import java.util.zip.Checksum; +import org.apache.commons.io.IOUtils; +import seng302.models.Player; +import seng302.models.Yacht; +import seng302.models.stream.packets.PacketType; +import seng302.models.stream.packets.StreamPacket; +import seng302.models.xml.Race; +import seng302.models.xml.Regatta; +import seng302.models.xml.XMLGenerator; +import seng302.server.messages.BoatActionType; +import seng302.server.messages.BoatLocationMessage; +import seng302.server.messages.BoatStatus; +import seng302.server.messages.BoatSubMessage; import seng302.server.messages.Message; import java.io.*; import java.net.Socket; import java.util.zip.CRC32; import java.util.zip.Checksum; +import seng302.server.messages.RaceStatus; +import seng302.server.messages.RaceStatusMessage; +import seng302.server.messages.RaceType; +import seng302.server.messages.XMLMessage; +import seng302.server.messages.XMLMessageSubType; +import seng302.server.messages.XMLMessage; +import seng302.server.messages.XMLMessageSubType; +import seng302.utilities.GeoPoint; /** - * A class describing a single connection to a Client for the purposes of sending and receiving on its own thread. - * All server threads created and owned by the server thread handler which can trigger client updates on its threads - * Created by wmu16 on 13/07/17. + * A class describing a single connection to a Client for the purposes of sending and receiving on + * its own thread. All server threads created and owned by the server thread handler which can + * trigger client updates on its threads Created by wmu16 on 13/07/17. */ -public class ServerToClientThread extends Thread { +public class ServerToClientThread implements Runnable, Observer { + private static final Integer LOG_LEVEL = 1; private static final Integer MAX_ID_ATTEMPTS = 10; + private Thread thread; + private InputStream is; private OutputStream os; private Socket socket; - private ByteArrayOutputStream crcBuffer; + 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 XMLGenerator xml; public ServerToClientThread(Socket socket) { this.socket = socket; @@ -38,39 +78,69 @@ public class ServerToClientThread extends Thread { } catch (IOException e) { System.out.println("IO error in server thread upon grabbing streams"); } -// threeWayHandshake(); - GameState.addPlayer(new Player(socket)); + //Attempt threeway handshake with connection + sourceId = GameState.getUniquePlayerID(); + if (threeWayHandshake(sourceId)) { + serverLog("Successful handshake. Client allocated id: " + sourceId, 1); + Yacht yacht = new Yacht("Yacht", sourceId, sourceId.toString(), "Kapa", "Kappa", "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); + } + } + + @Override + public void update(Observable o, Object arg) { + sendSetupMessages(); } public void run() { int sync1; int sync2; // TODO: 14/07/17 wmu16 - Work out how to fix this while loop - while(true) { - //System.out.print("."); + + while (socket.isConnected()) { try { +// if (initialisedRace) { +// sendSetupMessages(); +// initialisedRace = false; +// } + //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); - +// 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(); - //checking if it is the start of the packet - if(sync1 == 0x47 && sync2 == 0x83) { + if (sync1 == 0x47 && sync2 == 0x83) { int type = readByte(); //No. of milliseconds since Jan 1st 1970 long timeStamp = Message.bytesToLong(getBytes(6)); @@ -83,14 +153,22 @@ public class ServerToClientThread extends Thread { long packetCrc = Message.bytesToLong(getBytes(4)); if (computedCrc == packetCrc) { //System.out.println("RECEIVED A PACKET"); - StreamParser.parsePacket(new StreamPacket(type, payloadLength, timeStamp, payload)); - // TODO: 17/07/17 wmu16 - Fix this or maybe we dont need to go through the main server at all!?!? + switch (PacketType.assignPacketType(type)) { + case BOAT_ACTION: + BoatActionType actionType = ServerPacketParser + .extractBoatAction( + new StreamPacket(type, payloadLength, timeStamp, payload)); + GameState.updateBoat(sourceId, actionType); + break; + } } else { - System.err.println("Packet has been dropped"); + serverLog("Packet has been dropped", 1); } } } catch (Exception e) { - e.printStackTrace(); + // TODO: 24/07/17 zyt10 - fix a logic here when a client disconnected +// serverLog("ERROR OCCURRED, CLOSING SERVER CONNECTION: " + socket.getRemoteSocketAddress().toString(), 1); +// e.printStackTrace(); closeSocket(); return; } @@ -98,7 +176,34 @@ public class ServerToClientThread extends Thread { } + private void sendSetupMessages() { + xml = new XMLGenerator(); + Race race = new Race(); + + for (Yacht yacht : GameState.getYachts().values()) { + race.addBoat(yacht); + } + + //@TODO calculate lat/lng values + xml.setRegatta(new Regatta("RaceVision Test Game", 57.6679590, 11.8503233)); + xml.setRace(race); + + XMLMessage xmlMessage = new XMLMessage(xml.getRegattaAsXml(), XMLMessageSubType.REGATTA, + xml.getRegattaAsXml().length()); + sendMessage(xmlMessage); + + xmlMessage = new XMLMessage(xml.getBoatsAsXml(), XMLMessageSubType.BOAT, + xml.getBoatsAsXml().length()); + sendMessage(xmlMessage); + + xmlMessage = new XMLMessage(xml.getRaceAsXml(), XMLMessageSubType.RACE, + xml.getRaceAsXml().length()); + sendMessage(xmlMessage); +// System.out.println("Sent xml messages for " + thread.getName()); + } + public void updateClient() { + sendBoatLocationPackets(); updateClient = true; } @@ -109,28 +214,33 @@ public class ServerToClientThread extends Thread { * 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 void threeWayHandshake() { -// // TODO: 13/07/17 Finish using AC35 -// Integer playerID = GameState.getUniquePlayerID(); -// Integer confirmationID = null; -// Integer identificationAttempt = 0 -// while (!userIdentified) { -// os.write(playerID); //Send out new ID looking for echo -// confirmationID = is.read(); -// if (playerID == idConfirmation) { //ID is echoed back. Connection is a client -// os.write( some determined confirmation message ); //Confirm to client -// GameState.addPlayer(new Player(playerID, this)); //Create a player in game state for client -// userIdentified = true; -// } else if (identificationAttempt > MAX_ID_ATTEMPTS) { //No response. not a client. tidy up and go home. -// closeSocket(); -// return; -// } -// identificationAttempt++; -// } + 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) { + e.printStackTrace(); + } + + 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; } - public void closeSocket() { + private void closeSocket() { try { socket.close(); } catch (IOException e) { @@ -148,31 +258,101 @@ public class ServerToClientThread extends Thread { } catch (IOException e) { e.printStackTrace(); } - if (currentByte == -1){ + if (currentByte == -1) { throw new Exception(); } return currentByte; } - private byte[] getBytes(int n) throws Exception{ + private byte[] getBytes(int n) throws Exception { byte[] bytes = new byte[n]; - for (int i = 0; i < n; i++){ + for (int i = 0; i < n; i++) { bytes[i] = (byte) readByte(); } return bytes; } - private void skipBytes(long n) throws Exception{ - for (int i=0; i < n; i++){ + private void skipBytes(long n) throws Exception { + for (int i = 0; i < n; i++) { readByte(); } } - public void sendMessage(Message message){ + public void sendMessage(Message message) { try { os.write(message.getBuffer()); + } catch (SocketException e) { + //serverLog("Player " + sourceId + " side socket disconnected", 0); + return; } catch (IOException e) { e.printStackTrace(); } } + + private int getSeqNo() { + seqNo++; + return seqNo; + } + + + private void sendBoatLocationPackets() { + ArrayList yachts = new ArrayList<>(GameState.getYachts().values()); + for (Yacht yacht : yachts) { +// System.out.println("[SERVER] Lat: " + yacht.getLocation().getLat() + " Lon: " + yacht.getLocation().getLng()); + BoatLocationMessage boatLocationMessage = + new BoatLocationMessage( + yacht.getSourceId(), + getSeqNo(), + yacht.getLocation().getLat(), + yacht.getLocation().getLng(), + yacht.getHeading(), + (long) yacht.getVelocity()); + + sendMessage(boatLocationMessage); + } + } + + public Thread getThread() { + return thread; + } + + public void sendRaceStatusMessage() { + // variables taken from GameServerThread + int TIME_TILL_RACE_START = 20 * 1000; + long startTime = System.currentTimeMillis() + TIME_TILL_RACE_START; + + 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); + boatSubMessages.add(m); + } + + if (GameState.getCurrentStage() == GameStages.RACING) { + raceStatus = RaceStatus.STARTED; + } else { + raceStatus = RaceStatus.WARNING; + } + + sendMessage(new RaceStatusMessage(1, raceStatus, startTime, GameState.getWindDirection(), + GameState.getWindSpeed().longValue(), GameState.getPlayers().size(), + RaceType.MATCH_RACE, 1, boatSubMessages)); + } + + public Socket getSocket() { + return socket; + } } diff --git a/src/main/java/seng302/model/Boat.java b/src/main/java/seng302/model/Boat.java index caed3621..caf676b4 100644 --- a/src/main/java/seng302/model/Boat.java +++ b/src/main/java/seng302/model/Boat.java @@ -6,10 +6,18 @@ import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyLongWrapper; import javafx.scene.paint.Color; import seng302.model.mark.Mark; +import static seng302.utilities.GeoUtility.getGeoCoordinate; import java.text.DateFormat; import java.text.SimpleDateFormat; +import javafx.scene.paint.Color; +import seng302.client.ClientPacketParser; +import seng302.controllers.RaceViewController; +import seng302.gameServer.GameState; +import seng302.models.mark.Mark; +import seng302.utilities.GeoPoint; + /** * Yacht class for the racing boat. * @@ -18,11 +26,17 @@ import java.text.SimpleDateFormat; */ public class Boat { + private final Double TURN_STEP = 5.0; + + private Double lastHeading; + private Boolean sailIn; + + // Used in boat group private Color colour = Color.BLACK; private String boatType; - private Integer sourceID; + private Integer sourceId; private String hullID; //matches HullNum in the XML spec. private String shortName; private String boatName; @@ -40,30 +54,167 @@ public class Boat { private ReadOnlyDoubleWrapper velocity = new ReadOnlyDoubleWrapper(); private ReadOnlyLongWrapper timeTillNext = new ReadOnlyLongWrapper(); private ReadOnlyLongWrapper timeSinceLastMark = new ReadOnlyLongWrapper(); + private String position; + private GeoPoint location; + private Double heading; + private Double velocity; + private Long timeTillNext; + private Long markRoundTime; // Mark rounding private Mark lastMarkRounded; private Mark nextMark; + + /** + * @param location latlon location of the boat stored in a geopoint + * @param heading heading of the boat in degrees from 0 to 365 with 0 being north + */ + public Yacht(GeoPoint location, Double heading) { + this.location = location; + this.heading = heading; + this.velocity = 0.0; + this.sailIn = false; + } + + + /** + * Used in EventTest and RaceTest. + * + * @param boatName Create a yacht object with name. + */ + public Yacht(String boatName, String shortName, GeoPoint location, Double heading) { + this.boatName = boatName; + this.shortName = shortName; + this.location = location; + this.heading = heading; + this.velocity = 0.0; + this.sailIn = false; + } + + /** + * Used in BoatGroupTest. + * + * @param boatName The name of the team sailing the boat + * @param boatVelocity The speed of the boat in meters/second + * @param shortName A shorter version of the teams name + */ + public Yacht(String boatName, double boatVelocity, String shortName, int id) { + this.boatName = boatName; + this.velocity = boatVelocity; + this.shortName = shortName; + this.sourceId = id; + this.sailIn = false; + } + + + public Yacht(String boatType, Integer sourceId, String hullID, String shortName, + String boatName, String country) { public Boat(String boatType, Integer sourceID, String hullID, String shortName, String boatName, String country) { this.boatType = boatType; - this.sourceID = sourceID; + this.sourceId = sourceId; this.hullID = hullID; this.shortName = shortName; this.boatName = boatName; this.country = country; + this.position = "-"; + this.sailIn = false; + this.location = new GeoPoint(57.670341, 11.826856); + this.heading = 120.0; + this.velocity = 50000.0; } + /** + * @param timeInterval since last update in milliseconds + */ + public void update(Long timeInterval) { + if (sailIn) { + Double secondsElapsed = timeInterval / 1000000.0; + Double thisHeading = ((double) Math.floorMod(heading.longValue(), 360L)); + Double windSpeedKnots = 0d; + Double boatSpeedInKnots = PolarTable.getBoatSpeed(windSpeedKnots, thisHeading); + velocity = boatSpeedInKnots / ClientPacketParser.MS_TO_KNOTS * 3000; + //System.out.println("velocity = " + velocity); + Double metersCovered = velocity * secondsElapsed; + location = getGeoCoordinate(location, heading, metersCovered); + } + } + + + public Double getHeading() { + return heading; + } + + public void adjustHeading(Double amount) { + lastHeading = heading; + // TODO: 24/07/17 wmu16 - '%' in java does remainder, we need modulo. All this must be changed here, this is why we have neg values! + heading = (heading + amount) % 360.0; + } + + public void tackGybe(Double windDirection) { + adjustHeading(-2 * ((heading - windDirection) % 360)); + } + + public void toggleSailIn() { + sailIn = !sailIn; + } + + public void turnUpwind() { + Double normalizedHeading = (heading - GameState.windDirection) % 360; + 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 = (heading - GameState.windDirection) % 360; + 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 String getBoatType() { return boatType; } - public Integer getSourceID() { - return sourceID; + 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; } @@ -76,6 +227,7 @@ public class Boat { } public String getCountry() { + if (country == null) return ""; return country; } @@ -92,6 +244,10 @@ public class Boat { } public void setLegNumber(Integer legNumber) { + if (colour != null && position != "-" && legNumber != this.legNumber&& RaceViewController.sparkLineStatus( + sourceId)) { + RaceViewController.updateYachtPositionSparkline(this, legNumber); + } this.legNumber = legNumber; } @@ -154,12 +310,12 @@ public class Boat { } public void setNextMark(Mark nextMark) { - this.nextMark = nextMark; - } + this.nextMark = nextMark; + } public Mark getNextMark(){ return nextMark; - } + } public Double getLat() { return lat; @@ -183,6 +339,8 @@ public class Boat { public void setHeading(Double heading) { this.heading = heading; + public Boolean getSailIn() { + return sailIn; } @Override @@ -190,6 +348,10 @@ public class Boat { return boatName; } + public GeoPoint getLocation() { + return location; + } + public void setTimeSinceLastMark (long timeSinceLastMark) { this.timeSinceLastMark.set(timeSinceLastMark); } diff --git a/src/main/java/seng302/model/Player.java b/src/main/java/seng302/model/Player.java index 9f18e9e9..0d11bc19 100644 --- a/src/main/java/seng302/model/Player.java +++ b/src/main/java/seng302/model/Player.java @@ -13,8 +13,9 @@ public class Player { private Integer lastMarkPassed; - public Player(Socket socket) { + public Player(Socket socket, Yacht yacht) { this.socket = socket; + this.yacht = yacht; } public Socket getSocket() { diff --git a/src/main/java/seng302/model/PolarTable.java b/src/main/java/seng302/model/PolarTable.java index e24de7e3..338fd856 100644 --- a/src/main/java/seng302/model/PolarTable.java +++ b/src/main/java/seng302/model/PolarTable.java @@ -1,6 +1,10 @@ package seng302.model; -import java.io.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Array; import java.util.ArrayList; import java.util.HashMap; @@ -123,7 +127,7 @@ public final class PolarTable { */ public static HashMap getOptimalUpwindVMG(Double thisWindSpeed) { - Double polarWindSpeed = getClosestMatch(thisWindSpeed); + Double polarWindSpeed = getClosestWindSpeedInPolar(thisWindSpeed); return upwindOptimal.get(polarWindSpeed); } @@ -135,30 +139,47 @@ public final class PolarTable { */ public static HashMap getOptimalDownwindVMG(Double thisWindSpeed) { - Double polarWindSpeed = getClosestMatch(thisWindSpeed); + Double polarWindSpeed = getClosestWindSpeedInPolar(thisWindSpeed); return downwindOptimal.get(polarWindSpeed); } - private static Double getClosestMatch(Double thisWindSpeed) { + public static Double getBoatSpeed(Double thisWindSpeed, Double thisHeading) { - ArrayList windValues = new ArrayList<>(polarTable.keySet()); + Double polarWindSpeed = getClosestWindSpeedInPolar(thisWindSpeed); + Double polarAngle = getClosestAngleInPolar(polarTable.get(polarWindSpeed), thisHeading); - Double lowerVal = windValues.get(0); - Double upperVal = windValues.get(1); + return polarTable.get(polarWindSpeed).get(polarAngle); + } - for(int i = 0; i < windValues.size() - 1; i++) { - lowerVal = windValues.get(i); - upperVal = windValues.get(i+1); - if (thisWindSpeed <= upperVal) { - break; + + 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) { + smallestDif = difference; + closestWind = polarWindSpeed; } } + return closestWind; + } - Double lowerDiff = Math.abs(lowerVal - thisWindSpeed); - Double upperDiff = Math.abs(upperVal - thisWindSpeed); - return (lowerDiff <= upperDiff) ? lowerVal : upperVal; + public static Double getClosestAngleInPolar(HashMap thisWindSpeedPolar, Double thisHeading) { + Double smallestDif = Double.POSITIVE_INFINITY; + Double closestAngle = 0d; + + for (Double polarAngle : thisWindSpeedPolar.keySet()) { + Double difference = Math.abs(polarAngle - thisHeading); + if (difference < smallestDif) { + smallestDif = difference; + closestAngle = polarAngle; + } + } + return closestAngle; } } \ No newline at end of file diff --git a/src/main/java/seng302/models/xml/Race.java b/src/main/java/seng302/models/xml/Race.java new file mode 100644 index 00000000..9be61f37 --- /dev/null +++ b/src/main/java/seng302/models/xml/Race.java @@ -0,0 +1,53 @@ +package seng302.models.xml; + +import seng302.models.Yacht; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A Race object that can be parsed into XML + */ +public class Race { + private List yachts; + private LocalDateTime startTime; + + public Race(){ + yachts = new ArrayList<>(); + startTime = LocalDateTime.now(); + } + + /** + * Add a boat to the race + * @param yacht The boat to add + */ + public void addBoat(Yacht yacht){ + yachts.add(yacht); + } + + /** + * Get a list of boats in the race + * @return A List of boats + */ + public List getBoats(){ + return Collections.unmodifiableList(yachts); + } + + /** + * Set the time until the race starts + * @param seconds The time in seconds until the race starts + */ + public void setRaceStartDelay(Integer seconds){ + startTime = startTime.plusMinutes(seconds); + } + + /** + * Get the time the race starts + * @return The time the race starts + */ + public String getRaceStartTime(){ + return startTime.toString(); + } +} diff --git a/src/main/java/seng302/models/xml/Regatta.java b/src/main/java/seng302/models/xml/Regatta.java new file mode 100644 index 00000000..733b7a0a --- /dev/null +++ b/src/main/java/seng302/models/xml/Regatta.java @@ -0,0 +1,77 @@ +package seng302.models.xml; + +/** + * A Race regatta that can be parsed into XML + */ +public class Regatta { + private final Double DEFAULT_ALTITUDE = 0d; + private final Integer DEFAULT_REGATTA_ID = 0; + + private Integer id; + private String name; + private String courseName; + + private Double latitude; + private Double longitude; + private Double altitude; + + private Integer utcOffset; + private Double magneticVariation; + + public Regatta(String name, Double latitude, Double longitude) { + this.name = name; + this.id = DEFAULT_REGATTA_ID; + this.courseName = name; + + this.latitude = latitude; + this.longitude = longitude; + this.altitude = DEFAULT_ALTITUDE; + + this.utcOffset = 0; + this.magneticVariation = 0d; + } + + public void setMagneticVariation(Double magneticVariation){ + this.magneticVariation = magneticVariation; + } + + public void setUtcOffset(Integer offset){ + this.utcOffset = offset; + } + + /* + NOTE!! The following getters must follow the JavaBean standard (getPropertyName()), and must be public. + */ + + public String getName(){ + return name; + } + + public String getCourseName(){ + return courseName; + } + + public Integer getRegattaId(){ + return id; + } + + public Double getLatitude() { + return latitude; + } + + public Double getLongitude() { + return longitude; + } + + public Double getAltitude() { + return altitude; + } + + public Integer getUtcOffset(){ + return utcOffset; + } + + public Double getMagneticVariation(){ + return magneticVariation; + } +} diff --git a/src/main/java/seng302/models/xml/XMLGenerator.java b/src/main/java/seng302/models/xml/XMLGenerator.java new file mode 100644 index 00000000..04a5b5fb --- /dev/null +++ b/src/main/java/seng302/models/xml/XMLGenerator.java @@ -0,0 +1,161 @@ +package seng302.models.xml; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import org.apache.commons.io.IOUtils; +import seng302.server.messages.XMLMessageSubType; + +import java.io.*; +import java.net.URISyntaxException; + +/** + * An XML generator to generate the Race, Boat, and Regatta XML dynamically + */ +public class XMLGenerator { + private static final String XML_TEMPLATE_DIR = "/server_config/xml_templates"; + private static final String REGATTA_TEMPLATE_NAME = "regatta.ftlh"; + private static final String BOATS_TEMPLATE_NAME = "boats.ftlh"; + private static final String RACE_TEMPLATE_NAME = "race.ftlh"; + private Configuration configuration; + private Regatta regatta; + private Race race; + + /** + * Set up a configuration instance for Apache Freemake + */ + private void setupConfiguration() { + configuration = new Configuration(Configuration.VERSION_2_3_26); + + try { + configuration.setClassForTemplateLoading(getClass(), XML_TEMPLATE_DIR); + } catch (NullPointerException e){ + System.out.println("[FATAL] Server could not load XML Template directory, ensure this directory isn't empty"); + } + } + + /** + * Create an instance of the XML Generator + */ + public XMLGenerator(){ + setupConfiguration(); + } + + /** + * Set the race regatta to send to players + * Note: This must be set before a regatta message can be generated + * @param regatta The race regatta + */ + public void setRegatta(Regatta regatta){ + this.regatta = regatta; + } + + /** + * Set the race to send to players + * Note: This must be set before a boat or race message can be generated + * @param race The race + */ + public void setRace(Race race){ + this.race = race; + } + + /** + * Parse an XML template and generate the output as a string + * @param templateName The templates file name + * @param type The XML message sub type + */ + private String parseToXmlString(String templateName, XMLMessageSubType type) throws IOException, TemplateException { + Template template; + ByteArrayOutputStream os = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(os); + + template = configuration.getTemplate(templateName); + + switch (type) { + case REGATTA: + template.process(regatta, writer); + break; + + case BOAT: + template.process(race, writer); + break; + + case RACE: + template.process(race, writer); + break; + + default: + throw new UnsupportedOperationException(); + } + + try { + return os.toString("UTF-8"); + } catch (UnsupportedEncodingException e) { + System.out.println("[FATAL] UTF-8 Not supported"); + return null; + } + } + + /** + * Get the race regatta as a string + * Note: Regatta must be set before calling this + * @return String containing the regatta XML, null if there was an error + */ + public String getRegattaAsXml(){ + String result = null; + + if (regatta == null) return null; + + try { + result = parseToXmlString(REGATTA_TEMPLATE_NAME, XMLMessageSubType.REGATTA); + } catch (TemplateException e) { + System.out.println("[FATAL] Error parsing regatta"); + } catch (IOException e) { + System.out.println("[FATAL] Error reading regatta"); + } + + return result; + } + + /** + * Get the boats XML as a string + * Note: Race must be set before calling this + * @return String containing the boats XML, null if there was an error + */ + public String getBoatsAsXml() { + String result = null; + + if (race == null) return null; + + try { + result = parseToXmlString(BOATS_TEMPLATE_NAME, XMLMessageSubType.BOAT); + } catch (TemplateException e) { + System.out.println("[FATAL] Error parsing boats"); + } catch (IOException e) { + System.out.println("[FATAL] Error reading boats"); + } + + return result; + } + + /** + * Get the race XML as a string + * Note: Race must be set before calling this + * @return String containing the race XML, null if there was an error + */ + public String getRaceAsXml() { + String result = null; + + if (race == null) return null; + + try { + result = parseToXmlString(RACE_TEMPLATE_NAME, XMLMessageSubType.RACE); + } catch (TemplateException e) { + System.out.println("[FATAL] Error parsing race"); + } catch (IOException e) { + System.out.println("[FATAL] Error reading race"); + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/seng302/server/messages/BoatActionMessage.java b/src/main/java/seng302/server/messages/BoatActionMessage.java index 4a53f40a..cf4ea918 100644 --- a/src/main/java/seng302/server/messages/BoatActionMessage.java +++ b/src/main/java/seng302/server/messages/BoatActionMessage.java @@ -18,7 +18,7 @@ public class BoatActionMessage extends Message{ allocateBuffer(); writeHeaderToBuffer(); // Write message fields - putInt((int) BoatActionType.getBoatPacketType(actionType), 1); + putInt(actionType.getValue(), 1); writeCRC(); rewind(); diff --git a/src/main/java/seng302/server/messages/BoatActionType.java b/src/main/java/seng302/server/messages/BoatActionType.java index e323b6fe..f8318af7 100644 --- a/src/main/java/seng302/server/messages/BoatActionType.java +++ b/src/main/java/seng302/server/messages/BoatActionType.java @@ -1,5 +1,8 @@ package seng302.server.messages; +import java.util.HashMap; +import java.util.Map; + /** * Created by kre39 on 12/07/17. */ @@ -12,31 +15,24 @@ public enum BoatActionType { UPWIND(5), DOWNWIND(6); - private int type; + private final int type; + private static final Map intToTypeMap = new HashMap<>(); + + static { + for (BoatActionType type : BoatActionType.values()) { + intToTypeMap.put(type.getValue(), type); + } + } BoatActionType(int type){ this.type = type; } - public int getType(){ - return this.type; + public static BoatActionType getType(int value) { + return intToTypeMap.get(value); } - public static Short getBoatPacketType(BoatActionType type){ - switch (type){ - case VMG: - return 1; - case SAILS_IN: - return 2; - case SAILS_OUT: - return 3; - case TACK_GYBE: - return 4; - case UPWIND: - return 5; - case DOWNWIND: - return 6; - } - return 0; + public int getValue() { + return this.type; } } diff --git a/src/main/java/seng302/server/messages/RaceStatusMessage.java b/src/main/java/seng302/server/messages/RaceStatusMessage.java index 2375ba17..0310216e 100644 --- a/src/main/java/seng302/server/messages/RaceStatusMessage.java +++ b/src/main/java/seng302/server/messages/RaceStatusMessage.java @@ -9,12 +9,14 @@ public class RaceStatusMessage extends Message{ private final MessageType MESSAGE_TYPE = MessageType.RACE_STATUS; private final int MESSAGE_VERSION = 2; //Always set to 1 private final int MESSAGE_BASE_SIZE = 24; + private final double windDirFactor = 0x4000 / 90; + private long currentTime; private long raceId; private RaceStatus raceStatus; private long expectedStartTime; - private WindDirection raceWindDirection; + private double raceWindDirection; private long windSpeed; private long numBoatsInRace; private RaceType raceType; @@ -33,13 +35,13 @@ public class RaceStatusMessage extends Message{ * @param sourceId The source of this message * @param boats A list of boat status sub messages */ - public RaceStatusMessage(long raceId, RaceStatus raceStatus, long expectedStartTime, WindDirection raceWindDirection, + public RaceStatusMessage(long raceId, RaceStatus raceStatus, long expectedStartTime, double raceWindDirection, long windSpeed, long numBoatsInRace, RaceType raceType, long sourceId, List boats){ currentTime = System.currentTimeMillis(); this.raceId = raceId; this.raceStatus = raceStatus; this.expectedStartTime = expectedStartTime; - this.raceWindDirection = raceWindDirection; + this.raceWindDirection = raceWindDirection * windDirFactor; this.windSpeed = windSpeed; this.numBoatsInRace = numBoatsInRace; this.raceType = raceType; @@ -55,7 +57,7 @@ public class RaceStatusMessage extends Message{ putInt((int) raceId, 4); putByte((byte) raceStatus.getCode()); putInt(expectedStartTime, 6); - putInt((int) raceWindDirection.getCode(), 2); + putInt((int) this.raceWindDirection, 2); putInt((int) windSpeed, 2); putByte((byte) numBoatsInRace); putByte((byte) raceType.getCode()); diff --git a/src/main/java/seng302/server/messages/WindDirection.java b/src/main/java/seng302/server/messages/WindDirection.java deleted file mode 100644 index c0b8d767..00000000 --- a/src/main/java/seng302/server/messages/WindDirection.java +++ /dev/null @@ -1,20 +0,0 @@ -package seng302.server.messages; - -/** - * Enum containing the supported wind directions - */ -public enum WindDirection { - NORTH(0x0000L), - EAST(0x4000L), - SOUTH(0x8000L); - - private long code; - - WindDirection(long code) { - this.code = code; - } - - public long getCode() { - return code; - } -} diff --git a/src/main/java/seng302/visualiser/ClientToServerThread.java b/src/main/java/seng302/visualiser/ClientToServerThread.java index d67ac833..75c7ab1e 100644 --- a/src/main/java/seng302/visualiser/ClientToServerThread.java +++ b/src/main/java/seng302/visualiser/ClientToServerThread.java @@ -9,10 +9,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.net.UnknownHostException; +import java.time.LocalDateTime; import java.util.zip.CRC32; import java.util.zip.Checksum; import seng302.model.stream.packets.StreamPacket; +import seng302.models.stream.packets.StreamPacket; import seng302.server.messages.BoatActionMessage; import seng302.server.messages.Message; @@ -23,28 +26,45 @@ public class ClientToServerThread extends Thread { private Queue streamPackets = new ConcurrentLinkedQueue<>(); private List listeners = new ArrayList<>(); +public class ClientToServerThread implements Runnable { + + private static final int LOG_LEVEL = 1; + + private Thread thread; + + private Integer ourID; + private Socket socket; private InputStream is; private OutputStream os; - private final int PORT_NUMBER = 0; - private static final int LOG_LEVEL = 1; private Boolean updateClient = true; private ByteArrayOutputStream crcBuffer; - public ClientToServerThread(String ipAddress, Integer portNumber){ - try { - socket = new Socket(ipAddress, portNumber); - is = socket.getInputStream(); - os = socket.getOutputStream(); - } catch (IOException e) { - e.printStackTrace(); + public ClientToServerThread(String ipAddress, Integer portNumber) throws Exception{ + socket = new Socket(ipAddress, portNumber); + is = socket.getInputStream(); + os = socket.getOutputStream(); + + Integer allocatedID = threeWayHandshake(); + if (allocatedID != null) { + ourID = allocatedID; + clientLog("Successful handshake. Allocated ID: " + ourID, 1); + ClientState.setClientSourceId(String.valueOf(ourID)); + } else { + clientLog("Unsuccessful handshake", 1); + closeSocket(); + return; } + + thread = new Thread(this); + thread.start(); + } - static void serverLog(String message, int logLevel){ + static void clientLog(String message, int logLevel){ if(logLevel <= LOG_LEVEL){ - System.out.println("[SERVER] " + message); + System.out.println("[CLIENT " + LocalDateTime.now().toLocalTime().toString() + "] " + message); } } @@ -52,8 +72,18 @@ public class ClientToServerThread extends Thread { int sync1; int sync2; // TODO: 14/07/17 wmu16 - Work out how to fix this while loop - while(true) { + while(ClientState.isConnectedToHost()) { 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 +// try { +// GameState.outputState(os); +// } catch (IOException e) { +// System.out.println("IO error in server thread upon writing to output stream"); +// } + updateClient = false; + } crcBuffer = new ByteArrayOutputStream(); sync1 = readByte(); sync2 = readByte(); @@ -74,15 +104,42 @@ public class ClientToServerThread extends Thread { for (ClientSocketListener csl : listeners) csl.newPacket(new StreamPacket(type, payloadLength, timeStamp, payload)); } else { - System.err.println("Packet has been dropped"); + clientLog("Packet has been dropped", 1); } } } catch (Exception e) { closeSocket(); + e.printStackTrace(); return; } } + closeSocket(); + clientLog("Disconnected from server", 0); + } + + /** + * Listens for an allocated sourceID and returns it to the server if recieved + * @return the sourceID allocated to us by the server + */ + private Integer threeWayHandshake() { + Integer ourSourceID = null; + while (true) { + try { + ourSourceID = is.read(); + } catch (IOException e) { + e.printStackTrace(); + } + if (ourSourceID != null) { + try { + os.write(ourSourceID); + return ourSourceID; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + } } /** @@ -92,6 +149,7 @@ public class ClientToServerThread extends Thread { try { os.write(boatActionMessage.getBuffer()); } catch (IOException e) { + clientLog("COULD NOT WRITE TO SERVER", 0); e.printStackTrace(); } } @@ -140,4 +198,8 @@ public class ClientToServerThread extends Thread { readByte(); } } - } + + public Thread getThread() { + return thread; + } +} diff --git a/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java b/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java index e08521d4..7432853d 100644 --- a/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java +++ b/src/main/java/seng302/visualiser/controllers/FinishScreenViewController.java @@ -1,4 +1,4 @@ -package seng302.visualiser.controllers; +package seng302.controllers; import java.io.IOException; import java.net.URL; @@ -15,6 +15,9 @@ import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; +import seng302.client.ClientPacketParser; +import seng302.models.Yacht; +import seng302.models.stream.XMLParser.RaceXMLObject.Participant; import seng302.model.Boat; import seng302.model.stream.parsers.StreamParser; import seng302.model.stream.parsers.xml.XMLParser.RaceXMLObject.Participant; @@ -24,15 +27,15 @@ 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; @Override public void initialize(URL location, ResourceBundle resources) { @@ -41,7 +44,7 @@ public class FinishScreenViewController implements Initializable { finishOrderTable.getStylesheets().add(getClass().getResource("/css/master.css").toString()); // set up data for table - ObservableList data = FXCollections.observableArrayList(); + ObservableList data = FXCollections.observableArrayList(); finishOrderTable.setItems(data); // setting table col data @@ -67,7 +70,7 @@ public class FinishScreenViewController implements Initializable { } // add data to table - for (Boat boat : StreamParser.getBoatsPos().values()) { + for (Yacht boat : StreamParser.getBoatsPos().values()) { if (participantIDs.contains(boat.getSourceID())) { data.add(boat); } diff --git a/src/main/java/seng302/visualiser/controllers/LobbyController.java b/src/main/java/seng302/visualiser/controllers/LobbyController.java index efcb388a..00a9dec7 100644 --- a/src/main/java/seng302/visualiser/controllers/LobbyController.java +++ b/src/main/java/seng302/visualiser/controllers/LobbyController.java @@ -1,39 +1,92 @@ package seng302.visualiser.controllers; import java.io.IOException; +import java.io.InputStream; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.URL; -import java.util.Enumeration; -import java.util.ResourceBundle; +import java.util.*; + +import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; +import javafx.scene.control.Button; import javafx.scene.control.ListView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; import javafx.scene.text.Text; +import seng302.client.ClientState; +import seng302.client.ClientStateQueryingRunnable; import seng302.gameServer.GameStages; import seng302.gameServer.GameState; +import seng302.gameServer.MainServerThread; /** * A class describing the actions of the lobby screen * Created by wmu16 on 10/07/17. */ -public class LobbyController implements Initializable{ - - @FXML - private ListView competitorsListView; +public class LobbyController implements Initializable, Observer{ @FXML private GridPane lobbyScreen; @FXML private Text lobbyIpText; + @FXML + private Button readyButton; + @FXML + private ListView firstListView; + @FXML + private ListView secondListView; + @FXML + private ListView thirdListView; + @FXML + private ListView fourthListView; + @FXML + private ListView fifthListView; + @FXML + private ListView sixthListView; + @FXML + private ListView seventhListView; + @FXML + private ListView eighthListView; + @FXML + private ImageView firstImageView; + @FXML + private ImageView secondImageView; + @FXML + private ImageView thirdImageView; + @FXML + private ImageView fourthImageView; + @FXML + private ImageView fifthImageView; + @FXML + private ImageView sixthImageView; + @FXML + private ImageView seventhImageView; + @FXML + private ImageView eighthImageView; - private static ObservableList competitors; + private static List> competitors = new ArrayList<>(); + private static ObservableList firstCompetitor = FXCollections.observableArrayList(); + private static ObservableList secondCompetitor = FXCollections.observableArrayList(); + private static ObservableList thirdCompetitor = FXCollections.observableArrayList(); + private static ObservableList fourthCompetitor = FXCollections.observableArrayList(); + private static ObservableList fifthCompetitor = FXCollections.observableArrayList(); + private static ObservableList sixthCompetitor = FXCollections.observableArrayList(); + private static ObservableList seventhCompetitor = FXCollections.observableArrayList(); + private static ObservableList eighthCompetitor = FXCollections.observableArrayList(); + private ClientStateQueryingRunnable clientStateQueryingRunnable; + + private Boolean switchedPane = false; + private MainServerThread mainServerThread; private void setContentPane(String jfxUrl) { try { @@ -52,58 +105,162 @@ public class LobbyController implements Initializable{ @Override public void initialize(URL location, ResourceBundle resources) { - lobbyIpText.setText("Lobby Host IP: " + getLocalHostIp()); + if (ClientState.isHost()) { + lobbyIpText.setText("Lobby Host IP: " + ClientState.getHostIp()); + readyButton.setDisable(false); + } + else { + lobbyIpText.setText("Connected to IP: "); + readyButton.setDisable(true); + } + initialiseListView(); +// initialiseLobbyControllerThread(); + initialiseImageView(); // parrot gif init + + // set up client state query thread, so that when it receives the race-started packet + // it can switch to the race view + ClientStateQueryingRunnable clientStateQueryingRunnable = new ClientStateQueryingRunnable(); + clientStateQueryingRunnable.addObserver(this); + Thread clientStateQueryingThread = new Thread(clientStateQueryingRunnable, "Client State querying thread"); + clientStateQueryingThread.setDaemon(true); + clientStateQueryingThread.start(); } - public void initialize() { - competitors = FXCollections.observableArrayList(); - competitorsListView.setItems(competitors); - } - - private String getLocalHostIp() { - String ipAddress = null; - try { - Enumeration e = NetworkInterface.getNetworkInterfaces(); - while (e.hasMoreElements()) { - NetworkInterface ni = e.nextElement(); - if (ni.isLoopback()) - continue; - if(ni.isPointToPoint()) - continue; - if(ni.isVirtual()) - continue; - - Enumeration addresses = ni.getInetAddresses(); - while(addresses.hasMoreElements()) { - InetAddress address = addresses.nextElement(); - if(address instanceof Inet4Address) { // skip all ipv6 - ipAddress = address.getHostAddress(); - } + @Override + public void update(Observable o, Object arg) { + Platform.runLater(new Runnable() { + @Override + public void run() { + if (arg.equals("game started") && !switchedPane) { + switchToRaceView(); + } + if (arg.equals(("update players"))) { + initialiseListView(); } } - } catch (Exception e) { - e.printStackTrace(); + }); + } + + private void initialiseListView() { + firstListView.getItems().clear(); + secondListView.getItems().clear(); + thirdListView.getItems().clear(); + fourthListView.getItems().clear(); + fifthListView.getItems().clear(); + sixthListView.getItems().clear(); + seventhListView.getItems().clear(); + eighthListView.getItems().clear(); + + competitors = new ArrayList<>(); + Collections.addAll(competitors, firstCompetitor, secondCompetitor, thirdCompetitor, + fourthCompetitor, fifthCompetitor, sixthCompetitor, seventhCompetitor, eighthCompetitor); + + for (ObservableList ol : competitors) { + ol.removeAll(); } - if (ipAddress == null) { - System.out.println("[HOST] Cannot obtain local host ip address."); + + firstCompetitor.add(ClientState.getClientSourceId()); + + int competitorIndex = 1; + for (Integer yachtId : ClientState.getBoats().keySet()) { + // break if there are more than 7 competitors + if (competitorIndex >= 8) { + break; + } + if (!yachtId.equals(Integer.parseInt(ClientState.getClientSourceId()))) { + competitors.get(competitorIndex).add(String.valueOf(yachtId)); + competitorIndex++; + } } - return ipAddress; + + firstListView.setItems(firstCompetitor); + secondListView.setItems(secondCompetitor); + thirdListView.setItems(thirdCompetitor); + fourthListView.setItems(fourthCompetitor); + fifthListView.setItems(fifthCompetitor); + sixthListView.setItems(sixthCompetitor); + seventhListView.setItems(seventhCompetitor); + eighthListView.setItems(eighthCompetitor); + } + + private void initialiseLobbyControllerThread() { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + Platform.runLater(new Runnable() { + @Override + public void run() { + + } + }); + } + }); + thread.start(); + } + + private void initialiseImageView() { + Image image1 = new Image(getClass().getResourceAsStream("/ParrotGif/alistair.gif")); + firstImageView.setImage(image1); + Image image2 = new Image(getClass().getResourceAsStream("/ParrotGif/calum.gif")); + secondImageView.setImage(image2); + Image image3 = new Image(getClass().getResourceAsStream("/ParrotGif/haoming.gif")); + thirdImageView.setImage(image3); + Image image4 = new Image(getClass().getResourceAsStream("/ParrotGif/kusal.gif")); + fourthImageView.setImage(image4); + Image image5 = new Image(getClass().getResourceAsStream("/ParrotGif/michael.gif")); + fifthImageView.setImage(image5); + Image image6 = new Image(getClass().getResourceAsStream("/ParrotGif/peter.gif")); + sixthImageView.setImage(image6); + Image image7 = new Image(getClass().getResourceAsStream("/ParrotGif/ryan.gif")); + seventhImageView.setImage(image7); + Image image8 = new Image(getClass().getResourceAsStream("/ParrotGif/will.gif")); + eighthImageView.setImage(image8); } @FXML public void leaveLobbyButtonPressed() { // TODO: 10/07/17 wmu16 - Finish function! setContentPane("/views/StartScreenView.fxml"); - System.out.println("Leaving lobby!"); GameState.setCurrentStage(GameStages.CANCELLED); // TODO: 20/07/17 wmu16 - Implement some way of terminating the game + ClientState.setConnectedToHost(false); } - @FXML public void readyButtonPressed() { +// setContentPane("/views/RaceView.fxml"); + playTheme(); GameState.setCurrentStage(GameStages.RACING); - setContentPane("/views/RaceView.fxml"); + mainServerThread.startGame(); + } + + private static MediaPlayer mediaPlayer; + + private void playTheme() { + Random random = new Random(System.currentTimeMillis()); + Integer rand = random.nextInt(); + if(rand == 10) { + URL file = getClass().getResource("/music/Disturbed - down with the sickness.mp3"); + Media hit = new Media(file.toString()); + mediaPlayer = new MediaPlayer(hit); + mediaPlayer.play(); + } else if(rand == 9) { + URL file = getClass().getResource("/music/Owl City - Fireflies.mp3"); + Media hit = new Media(file.toString()); + mediaPlayer = new MediaPlayer(hit); + mediaPlayer.play(); + } + } + + private void switchToRaceView() { + if (!switchedPane) { + switchedPane = true; + setContentPane("/views/RaceView.fxml"); + } + } + + public void setMainServerThread(MainServerThread mainServerThread) { + this.mainServerThread = mainServerThread; } } diff --git a/src/main/java/seng302/visualiser/controllers/StartScreenController.java b/src/main/java/seng302/visualiser/controllers/StartScreenController.java index 90e2f2a6..27335485 100644 --- a/src/main/java/seng302/visualiser/controllers/StartScreenController.java +++ b/src/main/java/seng302/visualiser/controllers/StartScreenController.java @@ -1,11 +1,18 @@ package seng302.visualiser.controllers; +import java.net.Inet4Address; +import java.net.NetworkInterface; +import java.util.Enumeration; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; import javafx.scene.control.TextField; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; +import seng302.client.ClientState; +import seng302.client.ClientToServerThread; import seng302.visualiser.ClientToServerThread; import seng302.gameServer.GameState; import seng302.gameServer.MainServerThread; @@ -23,6 +30,8 @@ public class StartScreenController { @FXML private TextField ipTextField; @FXML + private TextField portTextField; + @FXML private GridPane startScreen2; /** @@ -38,6 +47,7 @@ public class StartScreenController { contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(jfxUrl)); contentPane.getChildren().addAll((Pane) fxmlLoader.load()); + return fxmlLoader.getController(); } catch (IOException e) { e.printStackTrace(); @@ -49,7 +59,8 @@ public class StartScreenController { /** * ATTEMPTS TO: * Sets up a new game state with your IP address as designated as the host. - * Starts a thread to listen for incoming connections + * Starts a thread to listen for incoming connections. + * Starts a client to server thread and connects to own ip. * Switches to the lobby screen */ @FXML @@ -62,26 +73,92 @@ public class StartScreenController { // controller.setClientToServerThread(clientToServerThread); clientToServerThread.start(); // get the lobby controller so that we can pass the game server thread to it - setContentPane("/views/LobbyView.fxml"); - - } catch (UnknownHostException e) { - System.err.println("COULD NOT FIND YOUR IP ADDRESS!"); + new GameState(getLocalHostIp()); + MainServerThread mainServerThread = new MainServerThread(); + ClientState.setHost(true); + // host will connect and handshake to itself after setting up the server + // TODO: 24/07/17 wmu16 - Make port number some static global type constant? + ClientToServerThread clientToServerThread = new ClientToServerThread(ClientState.getHostIp(), 4942); + ClientState.setConnectedToHost(true); + controller.setClientToServerThread(clientToServerThread); + LobbyController lobbyController = (LobbyController) setContentPane("/views/LobbyView.fxml"); + lobbyController.setMainServerThread(mainServerThread); + } catch (Exception e) { + Alert alert = new Alert(AlertType.ERROR); + alert.setHeaderText("Cannot host"); + alert.setContentText("Oops, failed to host, try to restart."); + alert.showAndWait(); e.printStackTrace(); } + } - + /** + * ATTEMPTS TO: + * Connect to an ip address and port using the ip and port specified on start screen. + * Starts a Client To Server Thread to maintain connection to host. + * Switch view to lobby view. + */ @FXML public void connectButtonPressed() { // TODO: 10/07/17 wmu16 - Finish function - String ipAddress = ipTextField.getText().trim().toLowerCase(); try { - ClientToServerThread clientToServerThread = new ClientToServerThread(ipAddress, 4950); - clientToServerThread.start(); + String ipAddress = ipTextField.getText().trim().toLowerCase(); + Integer port = Integer.valueOf(portTextField.getText().trim()); + + ClientToServerThread clientToServerThread = new ClientToServerThread(ipAddress, port); + ClientState.setHost(false); + ClientState.setConnectedToHost(true); + + controller.setClientToServerThread(clientToServerThread); setContentPane("/views/LobbyView.fxml"); - } catch (Exception e){ - e.printStackTrace(); + } catch (Exception e) { + Alert alert = new Alert(AlertType.ERROR); + alert.setHeaderText("Cannot reach the host"); + alert.setContentText("Please check your host IP address."); + alert.showAndWait(); } } + + public void setController(Controller controller) { + this.controller = controller; + } + + /** + * Gets the local host ip address and sets this ip to ClientState. + * Only runs by the host. + * + * @return the localhost ip address + */ + private String getLocalHostIp() { + String ipAddress = null; + try { + Enumeration e = NetworkInterface.getNetworkInterfaces(); + while (e.hasMoreElements()) { + NetworkInterface ni = e.nextElement(); + if (ni.isLoopback()) + continue; + if(ni.isPointToPoint()) + continue; + if(ni.isVirtual()) + continue; + + Enumeration addresses = ni.getInetAddresses(); + while(addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if(address instanceof Inet4Address) { // skip all ipv6 + ipAddress = address.getHostAddress(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + if (ipAddress == null) { + System.out.println("[HOST] Cannot obtain local host ip address."); + } + ClientState.setHostIp(ipAddress); + return ipAddress; + } } diff --git a/src/main/resources/ParrotGif/alistair.gif b/src/main/resources/ParrotGif/alistair.gif new file mode 100644 index 00000000..ce003789 Binary files /dev/null and b/src/main/resources/ParrotGif/alistair.gif differ diff --git a/src/main/resources/ParrotGif/calum.gif b/src/main/resources/ParrotGif/calum.gif new file mode 100644 index 00000000..03d495ed Binary files /dev/null and b/src/main/resources/ParrotGif/calum.gif differ diff --git a/src/main/resources/ParrotGif/haoming.gif b/src/main/resources/ParrotGif/haoming.gif new file mode 100644 index 00000000..33b26de6 Binary files /dev/null and b/src/main/resources/ParrotGif/haoming.gif differ diff --git a/src/main/resources/ParrotGif/kusal.gif b/src/main/resources/ParrotGif/kusal.gif new file mode 100644 index 00000000..1372928c Binary files /dev/null and b/src/main/resources/ParrotGif/kusal.gif differ diff --git a/src/main/resources/ParrotGif/michael.gif b/src/main/resources/ParrotGif/michael.gif new file mode 100644 index 00000000..83ea1dff Binary files /dev/null and b/src/main/resources/ParrotGif/michael.gif differ diff --git a/src/main/resources/ParrotGif/parrot.gif b/src/main/resources/ParrotGif/parrot.gif new file mode 100644 index 00000000..458ad859 Binary files /dev/null and b/src/main/resources/ParrotGif/parrot.gif differ diff --git a/src/main/resources/ParrotGif/peter.gif b/src/main/resources/ParrotGif/peter.gif new file mode 100644 index 00000000..470b46cf Binary files /dev/null and b/src/main/resources/ParrotGif/peter.gif differ diff --git a/src/main/resources/ParrotGif/ryan.gif b/src/main/resources/ParrotGif/ryan.gif new file mode 100644 index 00000000..b518c777 Binary files /dev/null and b/src/main/resources/ParrotGif/ryan.gif differ diff --git a/src/main/resources/ParrotGif/will.gif b/src/main/resources/ParrotGif/will.gif new file mode 100644 index 00000000..e7b762de Binary files /dev/null and b/src/main/resources/ParrotGif/will.gif differ diff --git a/src/main/resources/config/course.xml b/src/main/resources/config/course.xml index cec726ad..180f692a 100644 --- a/src/main/resources/config/course.xml +++ b/src/main/resources/config/course.xml @@ -12,8 +12,8 @@ Start2 - 57.6706330 - 11.8281330 + 57.6703330 + 11.8271333 123 diff --git a/src/main/resources/music/Disturbed - down with the sickness.mp3 b/src/main/resources/music/Disturbed - down with the sickness.mp3 new file mode 100644 index 00000000..375b140b Binary files /dev/null and b/src/main/resources/music/Disturbed - down with the sickness.mp3 differ diff --git a/src/main/resources/music/Owl City - Fireflies.mp3 b/src/main/resources/music/Owl City - Fireflies.mp3 new file mode 100644 index 00000000..fce20269 Binary files /dev/null and b/src/main/resources/music/Owl City - Fireflies.mp3 differ diff --git a/src/main/resources/server_config/boats1.xml b/src/main/resources/server_config/boats1.xml new file mode 100644 index 00000000..401e7bf6 --- /dev/null +++ b/src/main/resources/server_config/boats1.xml @@ -0,0 +1,171 @@ + + + 2015-08-28T17:32:59+0100 + 12 + 219 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/server_config/boats2.xml b/src/main/resources/server_config/boats2.xml new file mode 100644 index 00000000..c7255771 --- /dev/null +++ b/src/main/resources/server_config/boats2.xml @@ -0,0 +1,161 @@ + + + 2015-08-28T17:32:59+0100 + 12 + 219 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/server_config/boats3.xml b/src/main/resources/server_config/boats3.xml new file mode 100644 index 00000000..401e7bf6 --- /dev/null +++ b/src/main/resources/server_config/boats3.xml @@ -0,0 +1,171 @@ + + + 2015-08-28T17:32:59+0100 + 12 + 219 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/server_config/xml_templates/boats.ftlh b/src/main/resources/server_config/xml_templates/boats.ftlh new file mode 100644 index 00000000..f9a1e2d0 --- /dev/null +++ b/src/main/resources/server_config/xml_templates/boats.ftlh @@ -0,0 +1,22 @@ + + 2012-05-17T07:49:40+0200 + 12 + + + + + + + + <#-- Not used --> + + + <#list boats as boat> + + + + + + + \ No newline at end of file diff --git a/src/main/resources/server_config/xml_templates/race.ftlh b/src/main/resources/server_config/xml_templates/race.ftlh new file mode 100644 index 00000000..4349d2e3 --- /dev/null +++ b/src/main/resources/server_config/xml_templates/race.ftlh @@ -0,0 +1,86 @@ + + + ${raceStartTime} + + 15082901 + Fleet + + + <#list boats as boat> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/server_config/xml_templates/regatta.ftlh b/src/main/resources/server_config/xml_templates/regatta.ftlh new file mode 100644 index 00000000..25543c15 --- /dev/null +++ b/src/main/resources/server_config/xml_templates/regatta.ftlh @@ -0,0 +1,11 @@ + + + ${regattaId} + ${name} + ${courseName} + ${latitude} + ${longitude} + ${altitude} + ${utcOffset} + ${magneticVariation} + \ No newline at end of file diff --git a/src/main/resources/views/LobbyView.fxml b/src/main/resources/views/LobbyView.fxml index 454f3ff6..e867a519 100644 --- a/src/main/resources/views/LobbyView.fxml +++ b/src/main/resources/views/LobbyView.fxml @@ -1,5 +1,6 @@ + @@ -13,14 +14,14 @@ - + - - + + @@ -28,7 +29,7 @@ - + @@ -37,31 +38,100 @@ - + + + @@ -44,5 +47,13 @@ + + + + + + + + diff --git a/src/test/java/seng302/models/YachtTest.java b/src/test/java/seng302/models/YachtTest.java new file mode 100644 index 00000000..ab467522 --- /dev/null +++ b/src/test/java/seng302/models/YachtTest.java @@ -0,0 +1,27 @@ +package seng302.models; + + +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import seng302.utilities.GeoPoint; + +public class YachtTest { + + Double windDir; + Double windSpd; + List yachts = new ArrayList(); + + @Before + public void setUp() { + PolarTable.parsePolarFile(getClass().getResourceAsStream("/config/acc_polars.csv")); + windDir = 90d; + windSpd = 10d; + + yachts.add(new Yacht("Yacht 1", "Y1", new GeoPoint(-30.0, 20.0), 160.0)); + yachts.add(new Yacht("Yacht 2", "Y2", new GeoPoint(-40.0, -20.0), 100.0)); + yachts.add(new Yacht("Yacht 3", "Y3", new GeoPoint(-35.0, -15.5), 20.0)); + } + +}