WIP: Adapted the old server thread class to the GameServerThread class to allow multiple clients to connect

tags: #story[1047]  #pair[wmu16]
This commit is contained in:
William Muir
2017-07-13 14:40:11 +12:00
parent 035841f221
commit 8090cd7985
9 changed files with 138 additions and 387 deletions
-44
View File
@@ -35,51 +35,7 @@ public class App extends Application {
}
public static void main(String[] args) {
StreamReceiver sr = null;
new ServerThread("Racevision Test Server");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (args.length == 1 && args[0].equals("-standalone")) {
return;
}
if (args.length == 3 && args[0].equals("-server")) {
sr = new StreamReceiver(args[1], Integer.valueOf(args[2]), "RaceStream");
} else if (args.length == 2 && args[0].equals("-server")) {
switch (args[1]) {
case "internal":
sr = new StreamReceiver("localhost", 4949, "RaceStream");
break;
case "staffserver":
sr = new StreamReceiver("csse-s302staff.canterbury.ac.nz", 4941, "RaceStream");
break;
case "official":
sr = new StreamReceiver("livedata.americascup.com", 4941, "RaceStream");
break;
}
}
//Change the StreamReceiver in this else block to change the default data source.
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.start();
// StreamParser streamParser = new StreamParser("StreamParser");
// streamParser.start();
launch(args);
}
}
@@ -33,7 +33,7 @@ public class Controller implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) {
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
setContentPane("/views/StartScreen2View.fxml");
setContentPane("/views/StartScreenView.fxml");
StreamParser.boatLocations.clear();
}
}
@@ -52,7 +52,7 @@ public class LobbyController {
@FXML
public void leaveLobbyButtonPressed() {
// TODO: 10/07/17 wmu16 - Finish function!
setContentPane("/views/StartScreen2View.fxml");
setContentPane("/views/StartScreenView.fxml");
System.out.println("Leaving lobby!");
}
@@ -1,73 +0,0 @@
package seng302.controllers;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import seng302.gameServer.GameServerThread;
import seng302.gameServer.GameState;
import seng302.models.stream.StreamReceiver;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* A Class describing the actions of the start screen controller
* Created by wmu16 on 10/07/17.
*/
public class StartScreen2Controller {
@FXML
private TextField ipTextField;
@FXML
private GridPane startScreen2;
private void setContentPane(String jfxUrl) {
try {
AnchorPane contentPane = (AnchorPane) startScreen2.getParent();
contentPane.getChildren().removeAll();
contentPane.getChildren().clear();
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
contentPane.getChildren()
.addAll((Pane) FXMLLoader.load(getClass().getResource(jfxUrl)));
} catch (javafx.fxml.LoadException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 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
* Switches to the lobby screen
*/
@FXML
public void hostButtonPressed() {
try {
String ipAddress = InetAddress.getLocalHost().getHostAddress();
new GameState(ipAddress);
GameServerThread gameServerThread = new GameServerThread("Game Server");
setContentPane("/views/LobbyView.fxml");
} catch (UnknownHostException e) {
System.err.println("COULD NOT FIND YOUR IP ADDRESS!");
e.printStackTrace();
}
}
@FXML
public void connectButtonPressed() {
// TODO: 10/07/17 wmu16 - Finish function
String ipAddress = ipTextField.getText().trim();
StreamReceiver sr = new StreamReceiver(ipAddress, GameServerThread.PORT_NUMBER, "HostStream");
sr.start();
}
}
@@ -1,59 +1,34 @@
package seng302.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
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.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import seng302.models.Yacht;
import seng302.models.stream.StreamParser;
import seng302.models.stream.XMLParser.RaceXMLObject.Participant;
import seng302.gameServer.GameServerThread;
import seng302.gameServer.GameState;
import seng302.models.stream.StreamReceiver;
public class StartScreenController implements Initializable {
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* A Class describing the actions of the start screen controller
* Created by wmu16 on 10/07/17.
*/
public class StartScreenController {
@FXML
private GridPane gridPane;
private TextField ipTextField;
@FXML
private Label timeTillLive;
@FXML
private Button streamButton;
@FXML
private Button switchToRaceViewButton;
@FXML
private TableView<Yacht> teamList;
@FXML
private TableColumn<Yacht, String> boatNameCol;
@FXML
private TableColumn<Yacht, String> shortNameCol;
@FXML
private TableColumn<Yacht, String> countryCol;
@FXML
private TableColumn<Yacht, String> posCol;
@FXML
private Label realTime;
private GridPane startScreen2;
private boolean switchedToRaceView = false;
private void setContentPane(String jfxUrl) {
try {
// get the main controller anchor pane (MainView.fxml)
AnchorPane contentPane = (AnchorPane) gridPane.getParent();
AnchorPane contentPane = (AnchorPane) startScreen2.getParent();
contentPane.getChildren().removeAll();
contentPane.getChildren().clear();
contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
@@ -66,126 +41,34 @@ public class StartScreenController implements Initializable {
}
}
@Override
public void initialize(URL location, ResourceBundle resources) {
gridPane.getStylesheets().add(getClass().getResource("/css/master.css").toString());
teamList.getStylesheets().add(getClass().getResource("/css/master.css").toString());
}
/**
* Running a timer to update the livestream status on welcome screen. Update interval is 1
* second.
* 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
* Switches to the lobby screen
*/
public void startStream() {
// reset boolean for switch to race view
switchedToRaceView = false;
if (StreamParser.isStreamStatus()) {
streamButton.setVisible(false);
realTime.setVisible(true);
timeTillLive.setVisible(true);
timeTillLive.setTextFill(Color.GREEN);
timeTillLive.setText("Connecting...");
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Platform.runLater(() -> {
if (StreamParser.isRaceStarted()) {
if (!switchedToRaceView) {
switchToRaceView();
}
timer.cancel();
}
if (StreamParser.isRaceFinished()) {
realTime.setText(StreamParser.getCurrentTimeString());
timeTillLive.setTextFill(Color.RED);
timeTillLive.setText("Race finished! Waiting for new race...");
switchToRaceViewButton.setDisable(true);
} else if (StreamParser.getTimeSinceStart() > 0) {
realTime.setText(StreamParser.getCurrentTimeString());
updateTeamList();
timeTillLive.setTextFill(Color.RED);
switchToRaceViewButton.setDisable(false);
String timerMinute = Long
.toString(StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long
.toString(StreamParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
}
String timerString = "-" + timerMinute + ":" + timerSecond;
timeTillLive.setText(timerString);
} else {
realTime.setText(StreamParser.getCurrentTimeString());
updateTeamList();
timeTillLive.setTextFill(Color.BLACK);
switchToRaceViewButton.setDisable(false);
String timerMinute = Long
.toString(-1 * StreamParser.getTimeSinceStart() / 60);
String timerSecond = Long
.toString(-1 * StreamParser.getTimeSinceStart() % 60);
if (timerSecond.length() == 1) {
timerSecond = "0" + timerSecond;
}
String timerString = timerMinute + ":" + timerSecond;
timeTillLive.setText(timerString);
}
});
}
}, 0, 1000);
} else {
timeTillLive.setText("Stream not available.");
timeTillLive.setTextFill(Color.RED);
}
@FXML
public void hostButtonPressed() {
try {
String ipAddress = InetAddress.getLocalHost().getHostAddress();
new GameState(ipAddress);
new GameServerThread("Game Server");
System.out.println("Server thread started");
setContentPane("/views/LobbyView.fxml");
} catch (UnknownHostException e) {
System.err.println("COULD NOT FIND YOUR IP ADDRESS!");
e.printStackTrace();
}
public void switchToRaceView() {
StreamParser.boatLocations.clear();
switchedToRaceView = true;
setContentPane("/views/RaceView.fxml");
}
private void updateTeamList() {
ObservableList<Yacht> data = FXCollections.observableArrayList();
teamList.setItems(data);
boatNameCol.setCellValueFactory(
new PropertyValueFactory<>("boatName")
);
shortNameCol.setCellValueFactory(
new PropertyValueFactory<>("shortName")
);
countryCol.setCellValueFactory(
new PropertyValueFactory<>("country")
);
posCol.setCellValueFactory(
new PropertyValueFactory<>("position")
);
// check if the boat is racing
ArrayList<Participant> participants = StreamParser.getXmlObject().getRaceXML()
.getParticipants();
ArrayList<Integer> participantIDs = new ArrayList<>();
for (Participant p : participants) {
participantIDs.add(p.getsourceID());
}
// add boats to the start screen list
if (StreamParser.isRaceStarted()) { // if race is started, use StreamParser.getBoatsPos()
for (Yacht boat : StreamParser.getBoatsPos().values()) {
if (participantIDs.contains(boat.getSourceID())) {
data.add(boat);
}
}
} else { // else use StreamParser.getBoats()
for (Yacht boat : StreamParser.getBoats().values()) {
if (participantIDs.contains(boat.getSourceID())) {
data.add(boat);
}
}
}
teamList.refresh();
@FXML
public void connectButtonPressed() {
// TODO: 10/07/17 wmu16 - Finish function
String ipAddress = ipTextField.getText().trim();
StreamReceiver sr = new StreamReceiver(ipAddress, GameServerThread.PORT_NUMBER, "HostStream");
sr.start();
}
}
@@ -141,6 +141,7 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel
Message heartbeat = new Heartbeat(seqNum);
try {
System.out.println("Sending heartbeat");
broadcast(heartbeat);
} catch (IOException e) {
e.printStackTrace();
@@ -163,6 +164,7 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel
if (startTime < System.currentTimeMillis() && GameState.getCurrentStage() != GameStages.RACING){
}
else{
System.out.println("Sending race start status");
broadcast(raceStartStatusMessage);
}
@@ -202,12 +204,15 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel
Message regatta = getXmlMessage("/server_config/regatta.xml", XMLMessageSubType.REGATTA);
if (raceData != null){
System.out.println("Sending RaceXML");
broadcast(raceData);
}
if (boatData != null){
System.out.println("Sending boatsXML");
broadcast(boatData);
}
if (regatta != null){
System.out.println("Sending regattaXML");
broadcast(regatta);
}
} catch (IOException e) {
@@ -226,6 +231,7 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel
try {
Message raceData = getXmlMessage("/server_config/courseLimits.xml", XMLMessageSubType.RACE);
if (raceData != null) {
System.out.println("Sending courseLimitsXML");
broadcast(raceData);
}
}catch (IOException e) {
@@ -242,37 +248,47 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel
try{
server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress("localhost", PORT_NUMBER));
serverListenThread = new ServerListenThread(server, this);
serverListenThread.start();
// 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();
acceptConnection();
acceptConnection();
// acceptConnection();
for (Player player : GameState.getPlayers()) {
System.out.println(player);
}
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;
}
// 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();
// sendXml();
startSendingRaceStartStatusMessages();
//startSendingRaceStatusMessages();
sendPostStartCourseXml();
@@ -303,6 +319,21 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel
// }
/**
* 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 = server.accept();
if (thisClient.socket() != null){
Player thisPlayer = new Player(thisClient);
GameState.addPlayer(thisPlayer);
}
} catch (IOException e) {
e.getMessage();
}
}
void unicast(Message message, SocketChannel client) throws IOException {
message.send(client); // TODO: 11/07/17 Do we incement seqNum for individual messages?
@@ -311,6 +342,7 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel
void broadcast(Message message) throws IOException{
for(Player player : GameState.getPlayers()) {
System.out.println("Sending message seqNo[" + seqNum + "] to Player: " + player.toString());
message.send(player.getSocketChannel());
}
seqNum++; // TODO: 11/07/17 Do we increment seqNum for every message or for the one message to everyone
+13
View File
@@ -2,6 +2,7 @@ package seng302.models;
import javafx.scene.paint.Color;
import java.io.IOException;
import java.nio.channels.SocketChannel;
/**
@@ -36,4 +37,16 @@ public class Player {
public Yacht getYacht() {
return yacht;
}
@Override
public String toString() {
String playerAddress = null;
try {
playerAddress = socketChannel.getRemoteAddress().toString();
} catch (IOException e) {
e.printStackTrace();
}
return playerAddress;
}
}
@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane fx:id="startScreen2" nodeOrientation="LEFT_TO_RIGHT" prefWidth="800.0" style="-fx-background-color: #2C2c36;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="seng302.controllers.StartScreen2Controller">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints minHeight="72.0" prefHeight="72.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="65.0" minHeight="36.0" prefHeight="46.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="108.0" minHeight="72.0" prefHeight="98.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="72.0" prefHeight="72.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label alignment="CENTER" text="Welcome to Race Vision" textFill="WHITE" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="BOTTOM">
<font>
<Font size="40.0" />
</font>
</Label>
<Button mnemonicParsing="false" onAction="#hostButtonPressed" prefHeight="25.0" prefWidth="175.0" text="Host" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" GridPane.rowIndex="2" />
<Button mnemonicParsing="false" onAction="#connectButtonPressed" prefHeight="25.0" prefWidth="147.0" text="Connect" GridPane.columnIndex="1" GridPane.rowIndex="4" />
<TextField fx:id="ipTextField" maxWidth="-Infinity" prefHeight="25.0" prefWidth="200.0" GridPane.halignment="RIGHT" GridPane.rowIndex="4">
<GridPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</TextField>
<Text fill="WHITE" strokeType="OUTSIDE" strokeWidth="0.0" text="OR" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" GridPane.rowIndex="3">
<font>
<Font size="21.0" />
</font>
</Text>
</children>
</GridPane>
+28 -40
View File
@@ -1,60 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.canvas.*?>
<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane fx:id="gridPane" nodeOrientation="LEFT_TO_RIGHT" prefWidth="800.0" style="-fx-background-color: #2C2c36;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="seng302.controllers.StartScreenController">
<GridPane fx:id="startScreen2" nodeOrientation="LEFT_TO_RIGHT" prefWidth="800.0" style="-fx-background-color: #2C2c36;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="seng302.controllers.StartScreenController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints percentHeight="10.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="52.0" minHeight="52.0" prefHeight="52.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="0.0" percentHeight="8.0" prefHeight="0.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="28.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="55.0" minHeight="55.0" percentHeight="9.0" prefHeight="55.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="0.0" minHeight="0.0" percentHeight="29.0" prefHeight="0.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="93.0" minHeight="72.0" prefHeight="72.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="283.0" minHeight="262.0" prefHeight="283.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints minHeight="72.0" prefHeight="72.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="65.0" minHeight="36.0" prefHeight="46.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="108.0" minHeight="72.0" prefHeight="98.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="72.0" prefHeight="72.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label alignment="CENTER" text="Welcome to Race Vision" textFill="WHITE" GridPane.halignment="CENTER" GridPane.valignment="BOTTOM">
<Label alignment="CENTER" text="Welcome to Race Vision" textFill="WHITE" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="BOTTOM">
<font>
<Font size="40.0" />
</font>
</Label>
<Label text="Your live AC35 livestream" textFill="WHITE" GridPane.halignment="CENTER" GridPane.rowIndex="1">
<font>
<Font size="20.0" />
</font>
</Label>
<Label text="Livestream Status:" textFill="WHITE" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="BOTTOM">
<font>
<Font size="28.0" />
</font>
</Label>
<Label fx:id="timeTillLive" text="0:00 minutes" visible="false" GridPane.halignment="CENTER" GridPane.rowIndex="4">
<font>
<Font size="27.0" />
</font>
</Label>
<Button fx:id="streamButton" mnemonicParsing="false" onAction="#startStream" styleClass="blue-ui-btn" text="Click to stream" GridPane.halignment="CENTER" GridPane.rowIndex="4" />
<Button fx:id="switchToRaceViewButton" disable="true" mnemonicParsing="false" onAction="#switchToRaceView" styleClass="blue-ui-btn" text="Watch Race" GridPane.halignment="CENTER" GridPane.rowIndex="7" GridPane.valignment="TOP" />
<TableView fx:id="teamList" maxWidth="661.0" prefHeight="324.0" prefWidth="629.0" styleClass="ui-table" GridPane.halignment="CENTER" GridPane.hgrow="NEVER" GridPane.rowIndex="5" GridPane.vgrow="NEVER">
<columns>
<TableColumn fx:id="posCol" editable="false" maxWidth="74.0" minWidth="74.0" prefWidth="74.0" resizable="false" sortable="false" text="Position" />
<TableColumn fx:id="boatNameCol" editable="false" maxWidth="171.0" minWidth="171.0" prefWidth="171.0" resizable="false" sortable="false" text="Boat Name" />
<TableColumn fx:id="shortNameCol" editable="false" maxWidth="155.18472290039062" minWidth="107.0" prefWidth="155.18472290039062" resizable="false" sortable="false" text="Short Name" />
<TableColumn fx:id="countryCol" editable="false" maxWidth="258.9999694824219" minWidth="147.0" prefWidth="258.9999694824219" resizable="false" sortable="false" text="Country" />
</columns>
<Button mnemonicParsing="false" onAction="#hostButtonPressed" prefHeight="25.0" prefWidth="175.0" text="Host" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" GridPane.rowIndex="2" />
<Button mnemonicParsing="false" onAction="#connectButtonPressed" prefHeight="25.0" prefWidth="147.0" text="Connect" GridPane.columnIndex="1" GridPane.rowIndex="4" />
<TextField fx:id="ipTextField" maxWidth="-Infinity" prefHeight="25.0" prefWidth="200.0" GridPane.halignment="RIGHT" GridPane.rowIndex="4">
<GridPane.margin>
<Insets top="10.0" />
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</GridPane.margin>
</TableView>
<Label fx:id="realTime" text="Local time" textFill="WHITE" visible="false" GridPane.halignment="CENTER" GridPane.rowIndex="3" GridPane.valignment="BOTTOM" />
</TextField>
<Text fill="WHITE" strokeType="OUTSIDE" strokeWidth="0.0" text="OR" GridPane.columnSpan="2147483647" GridPane.halignment="CENTER" GridPane.rowIndex="3">
<font>
<Font size="21.0" />
</font>
</Text>
</children>
</GridPane>