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> + + + + + + + + \ 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> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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