From 035841f22158be8e09b7a4be67c952c5f81607bb Mon Sep 17 00:00:00 2001 From: William Muir Date: Tue, 11 Jul 2017 17:03:32 +1200 Subject: [PATCH] WIP: Adapted the old server thread class to the GameServerThread class to allow multiple clients to connect tags: #story[1047] #pair[wmu16, mra106] --- src/main/java/seng302/App.java | 10 +- .../seng302/controllers/LobbyController.java | 5 +- .../controllers/StartScreen2Controller.java | 20 +- .../gameServer/ClientConnectionDelegate.java | 7 + .../gameServer/GameConnectionListener.java | 53 --- .../seng302/gameServer/GameServerThread.java | 370 ++++++++++++++++++ .../java/seng302/gameServer/GameStages.java | 23 ++ .../java/seng302/gameServer/GameState.java | 18 +- .../gameServer/ServerListenThread.java | 45 +++ src/main/java/seng302/models/Player.java | 49 +-- src/main/java/seng302/models/Yacht.java | 47 ++- 11 files changed, 515 insertions(+), 132 deletions(-) create mode 100644 src/main/java/seng302/gameServer/ClientConnectionDelegate.java delete mode 100644 src/main/java/seng302/gameServer/GameConnectionListener.java create mode 100644 src/main/java/seng302/gameServer/GameServerThread.java create mode 100644 src/main/java/seng302/gameServer/GameStages.java create mode 100644 src/main/java/seng302/gameServer/ServerListenThread.java diff --git a/src/main/java/seng302/App.java b/src/main/java/seng302/App.java index a9d4e967..4050a08f 100644 --- a/src/main/java/seng302/App.java +++ b/src/main/java/seng302/App.java @@ -70,13 +70,13 @@ public class App extends Application { else{ // sr = new StreamReceiver("localhost", 4949, "RaceStream"); // sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941, "RaceStream"); -// sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4942, "RaceStream"); - sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream"); + sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4942, "RaceStream"); +// sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream"); } - sr.start(); - StreamParser streamParser = new StreamParser("StreamParser"); - streamParser.start(); +// sr.start(); +// StreamParser streamParser = new StreamParser("StreamParser"); +// streamParser.start(); launch(args); diff --git a/src/main/java/seng302/controllers/LobbyController.java b/src/main/java/seng302/controllers/LobbyController.java index 1f65bd34..965d7703 100644 --- a/src/main/java/seng302/controllers/LobbyController.java +++ b/src/main/java/seng302/controllers/LobbyController.java @@ -8,6 +8,8 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.text.Text; +import seng302.gameServer.GameStages; +import seng302.gameServer.GameState; import java.io.IOException; @@ -58,7 +60,6 @@ public class LobbyController { @FXML public void readyButtonPressed() { - // TODO: 10/07/17 wmu16 - Finish function - System.out.println("LEts play!!"); + GameState.setCurrentStage(GameStages.RACING); } } diff --git a/src/main/java/seng302/controllers/StartScreen2Controller.java b/src/main/java/seng302/controllers/StartScreen2Controller.java index 890fcdc9..974d18fe 100644 --- a/src/main/java/seng302/controllers/StartScreen2Controller.java +++ b/src/main/java/seng302/controllers/StartScreen2Controller.java @@ -6,12 +6,12 @@ import javafx.scene.control.TextField; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; -import seng302.gameServer.GameConnectionListener; +import seng302.gameServer.GameServerThread; import seng302.gameServer.GameState; +import seng302.models.stream.StreamReceiver; import java.io.IOException; import java.net.InetAddress; -import java.net.Socket; import java.net.UnknownHostException; /** @@ -53,15 +53,11 @@ public class StartScreen2Controller { try { String ipAddress = InetAddress.getLocalHost().getHostAddress(); new GameState(ipAddress); - GameConnectionListener gameConnectionListener = new GameConnectionListener(); - gameConnectionListener.start(); + GameServerThread gameServerThread = new GameServerThread("Game Server"); setContentPane("/views/LobbyView.fxml"); } catch (UnknownHostException e) { System.err.println("COULD NOT FIND YOUR IP ADDRESS!"); e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - System.err.println("COULD NOT OPEN CONNECTION!"); } } @@ -71,13 +67,7 @@ public class StartScreen2Controller { public void connectButtonPressed() { // TODO: 10/07/17 wmu16 - Finish function String ipAddress = ipTextField.getText().trim(); - Socket host = null; - try { - host = new Socket(ipAddress, GameConnectionListener.GAME_HOST_PORT); - - } catch (IOException e) { - e.printStackTrace(); - } - System.out.println("connecting to: " + ipTextField.getText()); + StreamReceiver sr = new StreamReceiver(ipAddress, GameServerThread.PORT_NUMBER, "HostStream"); + sr.start(); } } diff --git a/src/main/java/seng302/gameServer/ClientConnectionDelegate.java b/src/main/java/seng302/gameServer/ClientConnectionDelegate.java new file mode 100644 index 00000000..dc0827fe --- /dev/null +++ b/src/main/java/seng302/gameServer/ClientConnectionDelegate.java @@ -0,0 +1,7 @@ +package seng302.gameServer; + +import seng302.models.Player; + +public interface ClientConnectionDelegate { + void clientConnected(Player player); +} diff --git a/src/main/java/seng302/gameServer/GameConnectionListener.java b/src/main/java/seng302/gameServer/GameConnectionListener.java deleted file mode 100644 index 0f59e832..00000000 --- a/src/main/java/seng302/gameServer/GameConnectionListener.java +++ /dev/null @@ -1,53 +0,0 @@ -package seng302.gameServer; - -import seng302.models.Player; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.channels.ServerSocketChannel; - -/** - * A class defining the lobby host that will wait for connections and update the GameState appropriately - * Created by wmu16 on 10/07/17. - */ -public class GameConnectionListener extends Thread{ - - public static final Integer GAME_HOST_PORT = 4950; - - private Thread t; - private ServerSocketChannel serverSocketChannel; - - - public GameConnectionListener() throws IOException { - serverSocketChannel = ServerSocketChannel.open(); - // TODO: 10/07/17 wmu16 - If you pres host, leave lobby, host, - an error is thrown as this port is already bound. - serverSocketChannel.socket().bind(new InetSocketAddress("localhost", GAME_HOST_PORT)); - } - - - /** - * Starts the listening thread - */ - public void start() { - if (t == null) { - t = new Thread(this, "GameConnectionListener"); - t.start(); - } - } - - - /** - * This listens for players connecting and adds them to the GameState object - * WHILE - max plaers is not exceeded AND the race has not started - */ - public void run() { - while(GameState.getPlayers().size() < GameState.MAX_NUM_PLAYERS && !GameState.getIsRaceStarted()) { - try { - GameState.addPlayer(new Player(serverSocketChannel.accept())); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - -} diff --git a/src/main/java/seng302/gameServer/GameServerThread.java b/src/main/java/seng302/gameServer/GameServerThread.java new file mode 100644 index 00000000..3f204e84 --- /dev/null +++ b/src/main/java/seng302/gameServer/GameServerThread.java @@ -0,0 +1,370 @@ +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 HEARTBEAT_PERIOD = 5000; + 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(); + } + + 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); + } + + + /** + * Starts sending heartbeat messages to the client + */ + private void startSendingHeartbeats() { + Timer t = new Timer(); + + t.schedule(new TimerTask() { + @Override + public void run() { + Message heartbeat = new Heartbeat(seqNum); + + try { + broadcast(heartbeat); + } catch (IOException e) { + e.printStackTrace(); + } + } + }, 0, HEARTBEAT_PERIOD); + } + + /** + * 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); + } + } + },25000); + //Delays the new course xml data for 25 seconds so the boats are able to pass the starting line + } + + public void run() { + ServerListenThread serverListenThread; + + try{ + server = ServerSocketChannel.open(); + server.socket().bind(new InetSocketAddress("localhost", PORT_NUMBER)); + serverListenThread = new ServerListenThread(server, this); + serverListenThread.start(); + } + catch (IOException e){ + serverLog("Failed to bind socket: " + e.getMessage(), 0); + } + while (hosting) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + if (GameState.getCurrentStage() == GameStages.RACING) { + System.out.println("Racing"); + //startSendingHeartbeats(); + sendXml(); + //startSendingRaceStartStatusMessages(); + //startSendingRaceStatusMessages(); + //sendPostStartCourseXml(); + } + + else if (GameState.getCurrentStage() == GameStages.FINISHED) { + + } + + startTime = System.currentTimeMillis() + TIME_TILL_RACE_START; + } + + startSendingHeartbeats(); + sendXml(); + startSendingRaceStartStatusMessages(); + //startSendingRaceStatusMessages(); + sendPostStartCourseXml(); + } + +// /** +// * 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); +// } + + + + void unicast(Message message, SocketChannel client) throws IOException { + message.send(client); // TODO: 11/07/17 Do we incement seqNum for individual messages? + } + + + void broadcast(Message message) throws IOException{ + for(Player player : GameState.getPlayers()) { + message.send(player.getSocketChannel()); + } + seqNum++; // TODO: 11/07/17 Do we increment seqNum for every message or for the one message to everyone + } + + @Override + public void clientConnected(Player player) { + if (GameState.getPlayers().size() < MAX_NUM_PLAYERS && GameState.getCurrentStage() == GameStages.LOBBYING) { + System.out.println("Hi"); + GameState.addPlayer(player); + } + } + + /** + * 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(); +// } + + //} +} diff --git a/src/main/java/seng302/gameServer/GameStages.java b/src/main/java/seng302/gameServer/GameStages.java new file mode 100644 index 00000000..c58c652d --- /dev/null +++ b/src/main/java/seng302/gameServer/GameStages.java @@ -0,0 +1,23 @@ +package seng302.gameServer; + +/** + * An enum describing the states of the game + * Created by wmu16 on 11/07/17. + */ +public enum GameStages { + + LOBBYING(0), + PRE_RACE(1), + RACING(2), + FINISHED(3); + + private long code; + + GameStages(long code) { + this.code = code; + } + + public long getCode(){ + return code; + } +} diff --git a/src/main/java/seng302/gameServer/GameState.java b/src/main/java/seng302/gameServer/GameState.java index cf16a47a..d36968fc 100644 --- a/src/main/java/seng302/gameServer/GameState.java +++ b/src/main/java/seng302/gameServer/GameState.java @@ -10,14 +10,15 @@ import java.util.ArrayList; */ public class GameState { - public static final Integer MAX_NUM_PLAYERS = 10; private static String hostIpAddress; private static ArrayList players; private static Boolean isRaceStarted; + private static GameStages currentStage; public GameState(String hostIpAddress) { GameState.hostIpAddress = hostIpAddress; players = new ArrayList<>(); + currentStage = GameStages.LOBBYING; isRaceStarted = false; } @@ -41,6 +42,13 @@ public class GameState { return isRaceStarted; } + public static GameStages getCurrentStage() { + return currentStage; + } + + public static void setCurrentStage(GameStages currentStage) { + GameState.currentStage = currentStage; + } /** * This iterates through all players and updates each players info to its new state based on its current data @@ -50,8 +58,8 @@ public class GameState { // TODO: 10/07/17 wmu16 - Update all player info } } - - - - + + + + } diff --git a/src/main/java/seng302/gameServer/ServerListenThread.java b/src/main/java/seng302/gameServer/ServerListenThread.java new file mode 100644 index 00000000..4bb8a66b --- /dev/null +++ b/src/main/java/seng302/gameServer/ServerListenThread.java @@ -0,0 +1,45 @@ +package seng302.gameServer; + +import com.sun.corba.se.spi.activation.Server; +import seng302.models.Player; + +import java.io.IOException; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +/** + * A class for a thread to listen to connections + * Created by wmu16 on 11/07/17. + */ +public class ServerListenThread extends Thread{ + private ServerSocketChannel socketChannel; + private ClientConnectionDelegate delegate; + + public ServerListenThread(ServerSocketChannel socketChannel, ClientConnectionDelegate delegate){ + this.socketChannel = socketChannel; + this.delegate = delegate; + } + + /** + * Listens for a connection and upon finding one, creates a Player object and adds it to the universal GameState + */ + private void acceptConnection() { + try { + SocketChannel thisClient = socketChannel.accept(); + if (thisClient.socket() != null){ + Player thisPlayer = new Player(thisClient); + GameState.addPlayer(thisPlayer); + + delegate.clientConnected(thisPlayer); + } + } catch (IOException e) { + e.getMessage(); + } + } + + public void run(){ + while (true){ + acceptConnection(); + } + } +} diff --git a/src/main/java/seng302/models/Player.java b/src/main/java/seng302/models/Player.java index 4070e1dc..ceae4029 100644 --- a/src/main/java/seng302/models/Player.java +++ b/src/main/java/seng302/models/Player.java @@ -11,61 +11,19 @@ import java.nio.channels.SocketChannel; public class Player { private SocketChannel socketChannel; - private Color color; - private Float xPos; - private Float yPos; - private Float heading; - private Float velocity; + private Yacht yacht; private Integer lastMarkPassed; public Player(SocketChannel socketChannel) { this.socketChannel = socketChannel; + } public SocketChannel getSocketChannel() { return socketChannel; } - public Color getColor() { - return color; - } - - public void setColor(Color color) { - this.color = color; - } - - public Float getxPos() { - return xPos; - } - - public void setxPos(Float xPos) { - this.xPos = xPos; - } - - public Float getyPos() { - return yPos; - } - - public void setyPos(Float yPos) { - this.yPos = yPos; - } - - public Float getHeading() { - return heading; - } - - public void setHeading(Float heading) { - this.heading = heading; - } - - public Float getVelocity() { - return velocity; - } - - public void setVelocity(Float velocity) { - this.velocity = velocity; - } public Integer getLastMarkPassed() { return lastMarkPassed; @@ -75,4 +33,7 @@ public class Player { this.lastMarkPassed = lastMarkPassed; } + public Yacht getYacht() { + return yacht; + } } diff --git a/src/main/java/seng302/models/Yacht.java b/src/main/java/seng302/models/Yacht.java index c11c7407..7ea7e97e 100644 --- a/src/main/java/seng302/models/Yacht.java +++ b/src/main/java/seng302/models/Yacht.java @@ -24,6 +24,10 @@ public class Yacht { private String shortName; private String boatName; private String country; + + // Situational data + + // Boat status private Integer boatStatus; private Integer legNumber; @@ -31,6 +35,9 @@ public class Yacht { private Integer penaltiesServed; private Long estimateTimeAtFinish; private String position; + private Double lat; + private Double lon; + private Float heading; private double velocity; private Long timeTillNext; private Long markRoundTime; @@ -191,17 +198,41 @@ public class Yacht { this.lastMarkRounded = lastMarkRounded; } + public void setNextMark(Mark nextMark) { + this.nextMark = nextMark; + } + + public Mark getNextMark(){ + return nextMark; + } + + public Double getLat() { + return lat; + } + + public void setLat(Double lat) { + this.lat = lat; + } + + public Double getLon() { + return lon; + } + + public void setLon(Double lon) { + this.lon = lon; + } + + public Float getHeading() { + return heading; + } + + public void setHeading(Float heading) { + this.heading = heading; + } + @Override public String toString() { return boatName; } - public void setNextMark(Mark nextMark) { - this.nextMark = nextMark; - } - - public Mark getNextMark(){ - return nextMark; - } - }