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/controllers/CanvasController.java b/src/main/java/seng302/controllers/CanvasController.java
index 5c69f846..c13c9bcb 100644
--- a/src/main/java/seng302/controllers/CanvasController.java
+++ b/src/main/java/seng302/controllers/CanvasController.java
@@ -321,7 +321,7 @@ public class CanvasController {
}
for (Yacht boat : boats.values()) {
- if (participantIDs.contains(boat.getSourceID())) {
+ if (participantIDs.contains(boat.getSourceId())) {
boat.setColour(Colors.getColor());
BoatGroup boatGroup = new BoatGroup(boat, boat.getColour());
boatGroups.add(boatGroup);
diff --git a/src/main/java/seng302/fxObjects/BoatGroup.java b/src/main/java/seng302/fxObjects/BoatGroup.java
index acf0c0cb..1f557318 100644
--- a/src/main/java/seng302/fxObjects/BoatGroup.java
+++ b/src/main/java/seng302/fxObjects/BoatGroup.java
@@ -316,7 +316,7 @@ public class BoatGroup extends Group {
* @return An array containing all ID's associated with this RaceObject.
*/
public long getRaceId() {
- return boat.getSourceID();
+ return boat.getSourceId();
}
public Group getWake () {
diff --git a/src/main/java/seng302/gameServer/GameServerThread.java b/src/main/java/seng302/gameServer/GameServerThread.java
new file mode 100644
index 00000000..aafb6c0b
--- /dev/null
+++ b/src/main/java/seng302/gameServer/GameServerThread.java
@@ -0,0 +1,368 @@
+package seng302.gameServer;
+
+import seng302.models.Player;
+import seng302.models.Yacht;
+import seng302.server.messages.*;
+import seng302.server.simulator.Boat;
+import seng302.server.simulator.Simulator;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.SocketOption;
+import java.net.SocketOptions;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.*;
+
+public class GameServerThread implements Runnable, Observer, ClientConnectionDelegate{
+
+ private static final Integer MAX_NUM_PLAYERS = 10;
+ public static final int PORT_NUMBER = 4950;
+
+ private Boolean hosting = true;
+
+ private ServerSocketChannel server;
+ private long startTime;
+ private short seqNum;
+
+ private final int RACE_STATUS_PERIOD = 1000/2;
+ private final int RACE_START_STATUS_PERIOD = 1000;
+ private final int BOAT_LOCATION_PERIOD = 1000/5;
+ private final int TIME_TILL_RACE_START = 20*1000;
+ private static final int LOG_LEVEL = 1;
+
+ public GameServerThread(String threadName){
+ Thread runner = new Thread(this, threadName);
+ runner.setDaemon(true);
+ seqNum = 0;
+
+ runner.start();
+ }
+
+ public static void serverLog(String message, int logLevel){
+ if(logLevel <= LOG_LEVEL){
+ System.out.println("[SERVER] " + message);
+ }
+ }
+
+ /**
+ * Creates and returns an XML Message from the file specified
+ * @param fileName The source XML file
+ * @param type The XML Message type
+ * @return The XML Message
+ */
+ private Message getXmlMessage(String fileName, XMLMessageSubType type){
+ String fileContents = null;
+
+ try {
+ InputStream thisStream = this.getClass().getResourceAsStream(fileName);
+ fileContents = new String(org.apache.commons.io.IOUtils.toByteArray(thisStream));
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (NullPointerException e){
+ return null;
+ }
+
+ if (fileContents != null){
+ return new XMLMessage(fileContents, type, seqNum);
+ }
+
+ return null;
+ }
+
+ /**
+ * @return Get a race status message for the current race
+ */
+ private Message getRaceStatusMessage(){
+
+ List boatSubMessages = new ArrayList<>();
+ BoatStatus boatStatus;
+ RaceStatus raceStatus;
+ boolean thereAreBoatsNotFinished = false;
+
+ for (Player player : GameState.getPlayers()){
+ Yacht y = player.getYacht();
+
+ if (GameState.getCurrentStage() == GameStages.PRE_RACE){
+ boatStatus = BoatStatus.PRESTART;
+ thereAreBoatsNotFinished = true;
+ }
+ else if(false){ //@TODO if boat has finished
+ boatStatus = BoatStatus.FINISHED;
+ }
+ else{
+ boatStatus = BoatStatus.PRESTART;
+ thereAreBoatsNotFinished = true;
+ }
+
+ BoatSubMessage m = new BoatSubMessage(y.getSourceId(), boatStatus, y.getLastMarkRounded().getId(), 0, 0, 1234l, 1234l);
+ boatSubMessages.add(m);
+ }
+
+ if (thereAreBoatsNotFinished){
+ if (GameState.getCurrentStage() == GameStages.RACING){
+ raceStatus = RaceStatus.STARTED;
+ }
+ else{
+ long currentTime = System.currentTimeMillis();
+ long timeDifference = startTime - currentTime;
+
+ if (timeDifference > 60*3){
+ raceStatus = RaceStatus.PRESTART;
+ }
+ else if (timeDifference > 60){
+ raceStatus = RaceStatus.WARNING;
+ }
+ else{
+ raceStatus = RaceStatus.PREPARATORY;
+ }
+ }
+ }
+ else{
+ raceStatus = RaceStatus.TERMINATED;
+ }
+
+ return new RaceStatusMessage(1, raceStatus, startTime, WindDirection.SOUTH,
+ 100, GameState.getPlayers().size(), RaceType.MATCH_RACE, 1, boatSubMessages);
+ }
+
+ /**
+ * Start sending race start status messages until race starts
+ */
+ private void startSendingRaceStartStatusMessages(){
+ Timer t = new Timer();
+ t.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ Message raceStartStatusMessage = new RaceStartStatusMessage(seqNum, startTime , 1,
+ RaceStartNotificationType.SET_RACE_START_TIME);
+ try {
+ if (startTime < System.currentTimeMillis() && GameState.getCurrentStage() != GameStages.RACING){
+ }
+ else{
+ broadcast(raceStartStatusMessage);
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }, 0, RACE_START_STATUS_PERIOD);
+ }
+
+ /**
+ * Start sending race start status messages until race starts
+ */
+ private void startSendingRaceStatusMessages(){
+
+ Timer t = new Timer();
+ t.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ Message raceStatusMessage = getRaceStatusMessage();
+ try {
+ broadcast(raceStatusMessage);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }, 0, RACE_STATUS_PERIOD);
+ }
+
+ /**
+ * Sends the race, boat, and regatta XML files to the client
+ */
+ private void sendXml(){
+ try{
+ Message raceData = getXmlMessage("/server_config/race.xml", XMLMessageSubType.RACE);
+ Message boatData = getXmlMessage("/server_config/boats.xml", XMLMessageSubType.BOAT);
+ Message regatta = getXmlMessage("/server_config/regatta.xml", XMLMessageSubType.REGATTA);
+
+ if (raceData != null){
+ broadcast(raceData);
+ }
+ if (boatData != null){
+ broadcast(boatData);
+ }
+ if (regatta != null){
+ broadcast(regatta);
+ }
+ } catch (IOException e) {
+ serverLog("Couldn't send an XML Message: " + e.getMessage(), 0);
+ }
+ }
+
+ /**
+ * Send the post-start race course information
+ */
+ private void sendPostStartCourseXml(){
+ Timer t = new Timer();
+ t.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ try {
+ Message raceData = getXmlMessage("/server_config/courseLimits.xml", XMLMessageSubType.RACE);
+ if (raceData != null) {
+ broadcast(raceData);
+ }
+ }catch (IOException e) {
+ serverLog("Couldn't send an XML Message: " + e.getMessage(), 0);
+ }
+ }
+ },1000);
+ //Delays the new course xml data for 25 seconds so the boats are able to pass the starting line
+ }
+
+ public void run() {
+ ServerListenThread serverListenThread;
+ HeartbeatThread heartbeatThread;
+ Boolean serverIsSendingMessages = false;
+
+ try{
+ server = ServerSocketChannel.open();
+ server.socket().bind(new InetSocketAddress("localhost", PORT_NUMBER));
+
+ serverListenThread = new ServerListenThread(server, this);
+ heartbeatThread = new HeartbeatThread(this);
+
+ heartbeatThread.start();
+ serverListenThread.start();
+ }
+ catch (IOException e){
+ serverLog("Failed to bind socket: " + e.getMessage(), 0);
+ }
+
+ while (hosting) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ if (GameState.getCurrentStage() == GameStages.RACING && !serverIsSendingMessages) {
+ serverLog("Race Started", 0);
+
+ sendXml();
+ startSendingRaceStartStatusMessages();
+ //startSendingRaceStatusMessages();
+ sendPostStartCourseXml();
+ serverIsSendingMessages = true;
+ }
+
+ else if (GameState.getCurrentStage() == GameStages.FINISHED) {
+ serverLog("Race Finished", 0);
+ }
+
+ startTime = System.currentTimeMillis() + TIME_TILL_RACE_START;
+ }
+ }
+
+// /**
+// * Start sending static boat position updates when race has finished
+// */
+// private void startSendingRaceFinishedBoatPositions(){
+// Timer t = new Timer();
+// t.schedule(new TimerTask() {
+// @Override
+// public void run() {
+// try {
+// for (Boat b : raceSimulator.getBoats()){
+// Message m = new BoatLocationMessage(b.getSourceID(), seqNum, b.getLat(),
+// b.getLng(), b.getLastPassedCorner().getBearingToNextCorner(),
+// ((long) 0));
+//
+// server.send(m);
+// }
+//
+// } catch (IOException e) {
+// e.printStackTrace();
+// }
+// }
+// }, 0, BOAT_LOCATION_PERIOD);
+// }
+
+ /**
+ * A client has tried to connect to the server
+ * @param player The player that connected
+ */
+ @Override
+ public void clientConnected(Player player) {
+ if (GameState.getPlayers().size() < MAX_NUM_PLAYERS && GameState.getCurrentStage() == GameStages.LOBBYING) {
+ serverLog("Player Connected", 0);
+ GameState.addPlayer(player);
+ sendXml();
+ }
+ }
+
+ /**
+ * A player has left the game, remove the player from the GameState
+ * @param player The player that left
+ */
+ @Override
+ public void clientDisconnected(Player player) {
+ serverLog("Player disconnected", 0);
+ GameState.removePlayer(player);
+ sendXml();
+ }
+
+
+ void broadcast(Message message) throws IOException{
+ for(Player player : GameState.getPlayers()) {
+ //heh
+ player.getSocketChannel().socket().getOutputStream().write(message.getBuffer());
+ }
+ seqNum++;
+ }
+
+ /**
+ * Send a boat location message when they are updated by the simulator
+ * @param o .
+ * @param arg .
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public void update(Observable o, Object arg) {
+ /* Only send if server started
+ // TODO: I don't understand why i need to check server is null or not ... confused - haoming 2/5/17
+ if(server == null || !server.isStarted()){
+ return;
+ }
+
+ int numOfBoatsFinished = 0;
+ for (Boat boat : (List) arg){
+ try {
+ if (boat.isFinished()) {
+ numOfBoatsFinished ++;
+ if (!boatsFinished.get(boat.getSourceID())) {
+ boatsFinished.put(boat.getSourceID(), true);
+ }
+ }
+ Message m = new BoatLocationMessage(boat.getSourceID(), 1, boat.getLat(),
+ boat.getLng(), boat.getLastPassedCorner().getBearingToNextCorner(),
+ ((long) boat.getSpeed()));
+ broadcast(m);
+ } catch (IOException e) {
+ serverLog("Couldn't send a boat status message", 3);
+ return;
+ }
+ catch (NullPointerException e){
+ e.printStackTrace();
+ }*/
+ }
+
+// if (numOfBoatsFinished == ((List) arg).size()) {
+// startSendingRaceFinishedBoatPositions();
+// }
+
+ //}
+
+ public void terminateGame() {
+ try {
+ //TODO: for now, I just close the socket, but i think we should terminate the whole thread instead. -hyi25 13 July
+ server.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/seng302/models/stream/XMLParser.java b/src/main/java/seng302/models/stream/XMLParser.java
index 99ce72c8..733bcb54 100644
--- a/src/main/java/seng302/models/stream/XMLParser.java
+++ b/src/main/java/seng302/models/stream/XMLParser.java
@@ -558,7 +558,7 @@ public class XMLParser {
getNodeAttributeString(currentBoat, "Country"));
this.boats.add(boat);
if (boat.getBoatType().equals("Yacht")) {
- competingBoats.put(boat.getSourceID(), boat);
+ competingBoats.put(boat.getSourceId(), boat);
}
}
}
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..d74dfb31
--- /dev/null
+++ b/src/main/java/seng302/models/xml/XMLGenerator.java
@@ -0,0 +1,164 @@
+package seng302.models.xml;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+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.setDirectoryForTemplateLoading(new File(getClass().getResource(XML_TEMPLATE_DIR).toURI()));
+ } catch (IOException e){
+ System.out.println("[FATAL] Server could not read XML templates");
+ } catch (URISyntaxException e) {
+ System.out.println("[FATAL] Xml template directory URI is invalid");
+ } 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/resources/server_config/xml_templates/boats.ftlh b/src/main/resources/server_config/xml_templates/boats.ftlh
new file mode 100644
index 00000000..2dc61eee
--- /dev/null
+++ b/src/main/resources/server_config/xml_templates/boats.ftlh
@@ -0,0 +1,28 @@
+
+
+
+ 2012-05-17T07:49:40+0200
+ 12
+
+
+
+
+
+
+
+
+
+ <#-- Not used -->
+
+
+
+ <#list boats as boat>
+
+
+
+
+
+ #list>
+
+
\ 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..6bdb6ef5
--- /dev/null
+++ b/src/main/resources/server_config/xml_templates/race.ftlh
@@ -0,0 +1,86 @@
+
+
+ ${raceStartTime}
+
+ 15082901
+ Fleet
+
+
+ <#list boats as boat>
+
+ #list>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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