Merge branch 'issue47_disconnect_crash_rebranch' into 'develop'

Issue47 disconnect crash rebranch

Fix for all disconnection issues. Fix is not robust. Need consistent interface between disconnection of ServerToClientThreads and MainServerThread.

See merge request !66
This commit is contained in:
Alistair McIntyre
2017-08-17 14:55:45 +12:00
10 changed files with 232 additions and 158 deletions
@@ -429,7 +429,6 @@ public class GameState implements Runnable {
private void checkForLegProgression(ServerYacht yacht) {
Integer currentMarkSeqID = yacht.getCurrentMarkSeqID();
CompoundMark currentMark = markOrder.getCurrentMark(currentMarkSeqID);
// System.out.println(yacht.getCurrentMarkSeqID());
Boolean hasProgressed;
if (currentMarkSeqID == 0) {
@@ -92,6 +92,9 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate {
// TODO: 14/07/17 wmu16 - Send out disconnect packet to clients
try {
for (ServerToClientThread serverToClientThread : serverToClientThreads) {
serverToClientThread.terminate();
}
serverSocket.close();
return;
} catch (IOException e) {
@@ -172,6 +175,7 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate {
thread.sendSetupMessages();
}
});
serverToClientThread.addDisconnectListener(this::clientDisconnected);
}
/**
@@ -181,11 +185,11 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate {
*/
@Override
public void clientDisconnected(Player player) {
try {
player.getSocket().close();
} catch (Exception e) {
serverLog("Cannot disconnect the socket for the disconnected player.", 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);
@@ -193,11 +197,12 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate {
for (ServerToClientThread serverToClientThread : serverToClientThreads) {
if (serverToClientThread.getSocket() == player.getSocket()) {
closedConnection = serverToClientThread;
} else {
} else if (GameState.getCurrentStage() != GameStages.RACING){
serverToClientThread.sendSetupMessages();
}
}
serverToClientThreads.remove(closedConnection);
closedConnection.terminate();
}
public void startGame() {
@@ -38,7 +38,7 @@ public class ServerListenThread implements Runnable {
}
public void run(){
while (true){
while (serverSocket != null && !serverSocket.isClosed()){
acceptConnection();
}
}
@@ -40,17 +40,20 @@ import seng302.model.stream.xml.generator.Regatta;
import seng302.utilities.XMLGenerator;
import seng302.gameServer.messages.BoatAction;
import seng302.gameServer.messages.BoatLocationMessage;
import seng302.gameServer.messages.BoatSubMessage;
import seng302.gameServer.messages.ClientType;
import seng302.gameServer.messages.Message;
import seng302.gameServer.messages.RaceStatus;
import seng302.gameServer.messages.RaceStatusMessage;
import seng302.gameServer.messages.RaceType;
import seng302.gameServer.messages.RegistrationResponseMessage;
import seng302.gameServer.messages.RegistrationResponseStatus;
import seng302.gameServer.messages.XMLMessage;
import seng302.gameServer.messages.XMLMessageSubType;
import seng302.gameServer.messages.YachtEventCodeMessage;
import seng302.model.Player;
import seng302.model.ServerYacht;
import seng302.model.stream.packets.PacketType;
import seng302.model.stream.packets.StreamPacket;
import seng302.model.stream.xml.generator.Race;
import seng302.model.stream.xml.generator.Regatta;
import seng302.utilities.XMLGenerator;
/**
* A class describing a single connection to a Client for the purposes of sending and receiving on
@@ -67,6 +70,12 @@ public class ServerToClientThread implements Runnable, Observer {
void notifyConnection ();
}
// TODO: 17/08/17 this is only temporary disconnects should be handled consistently
@FunctionalInterface
interface DisconnectListener {
void notifyDisconnect (Player player);
}
private Logger logger = LoggerFactory.getLogger(ServerToClientThread.class);
private Thread thread;
@@ -86,8 +95,10 @@ public class ServerToClientThread implements Runnable, Observer {
private XMLGenerator xml;
private List<ConnectionListener> connectionListeners = new ArrayList<>();
private DisconnectListener disconnectListener;
private ServerYacht yacht;
private Player player;
public ServerToClientThread(Socket socket) {
this.socket = socket;
@@ -134,8 +145,9 @@ public class ServerToClientThread implements Runnable, Observer {
);
yacht.addObserver(this); // TODO: yacht can notify mark rounding message hyi25 13/8/17
player = new Player(socket, yacht);
GameState.addYacht(sourceId, yacht);
GameState.addPlayer(new Player(socket, yacht));
GameState.addPlayer(player);
}
@Override
@@ -181,8 +193,7 @@ public class ServerToClientThread implements Runnable, Observer {
int sync2;
// TODO: 14/07/17 wmu16 - Work out how to fix this while loop
while (socket.isConnected()) {
while (socket.isConnected() && !socket.isClosed()) {
try {
crcBuffer = new ByteArrayOutputStream();
sync1 = readByte();
@@ -200,7 +211,6 @@ public class ServerToClientThread implements Runnable, Observer {
long computedCrc = checksum.getValue();
long packetCrc = Message.bytesToLong(getBytes(4));
if (computedCrc == packetCrc) {
//System.out.println("RECEIVED A PACKET");
switch (PacketType.assignPacketType(type, payload)) {
case BOAT_ACTION:
BoatAction actionType = ServerPacketParser
@@ -237,6 +247,7 @@ public class ServerToClientThread implements Runnable, Observer {
return;
}
}
logger.warn("Closed serverToClientThread" + thread, 1);
}
public void sendSetupMessages() {
@@ -248,7 +259,11 @@ public class ServerToClientThread implements Runnable, Observer {
}
//@TODO calculate lat/lng values
xml.setRegatta(new Regatta("Party Parrot Test Server", "Bermuda Test Course", 57.6679590, 11.8503233));
xml.setRegatta(
new Regatta(
"Party Parrot Test Server", "Bermuda Test Course",
57.6679590, 11.8503233)
);
xml.setRace(race);
XMLMessage xmlMessage;
@@ -276,10 +291,12 @@ public class ServerToClientThread implements Runnable, Observer {
private int readByte() throws Exception {
int currentByte = -1;
try {
// @TODO @FIX ConnectionReset Exception when a client disconnects before it is garbage collected
currentByte = is.read();
crcBuffer.write(currentByte);
} catch (SocketException se) {
disconnectListener.notifyDisconnect(this.player);
} catch (IOException e) {
disconnectListener.notifyDisconnect(this.player);
logger.warn("Socket read failed", 1);
}
if (currentByte == -1) {
@@ -306,8 +323,7 @@ public class ServerToClientThread implements Runnable, Observer {
try {
os.write(message.getBuffer());
} catch (SocketException e) {
//serverLog("Player " + sourceId + " side socket disconnected", 1);
return;
logger.warn("Player " + sourceId + " side socket disconnected", 1);
} catch (IOException e) {
logger.warn("Message send failed", 1);
}
@@ -358,4 +374,16 @@ public class ServerToClientThread implements Runnable, Observer {
public void removeConnectionListener(ConnectionListener listener) {
connectionListeners.remove(listener);
}
public void terminate () {
try {
socket.close();
} catch (IOException ioe) {
logger.warn("IOException attempting to terminate serverToClientThread " + this.thread);
}
}
public void addDisconnectListener(DisconnectListener disconnectListener) {
this.disconnectListener = disconnectListener;
}
}
@@ -5,7 +5,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -15,10 +14,6 @@ import java.util.TimerTask;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import seng302.gameServer.messages.BoatAction;
@@ -48,16 +43,20 @@ public class ClientToServerThread implements Runnable {
void newPacket();
}
@FunctionalInterface
public interface DisconnectedFromHostListener {
void notifYDisconnection (String message);
}
private class ByteReadException extends Exception {
private ByteReadException(String message) {
super(message);
}
}
private static final int LOG_LEVEL = 1;
private Queue<StreamPacket> streamPackets = new ConcurrentLinkedQueue<>();
private List<ClientSocketListener> listeners = new ArrayList<>();
private List<DisconnectedFromHostListener> disconnectionListeners = new ArrayList<>();
private Thread thread;
private Socket socket;
@@ -74,7 +73,6 @@ public class ClientToServerThread implements Runnable {
private int clientId = -1;
// private Boolean updateClient = true;
private ByteArrayOutputStream crcBuffer;
private boolean socketOpen = true;
@@ -101,20 +99,6 @@ public class ClientToServerThread implements Runnable {
thread.start();
}
/**
* Prints out log messages and the time happened.
* Only perform task if log level is below LOG_LEVEL variable.
*
* @param message a string of message to be printed out
* @param logLevel an int for log level
*/
static void clientLog(String message, int logLevel) {
if (logLevel <= LOG_LEVEL) {
System.out.println(
"[CLIENT " + LocalDateTime.now().toLocalTime().toString() + "] " + message);
}
}
/**
* Perform the thread loop. It exits the loop if ClientState connected to host
* variable is false.
@@ -123,7 +107,7 @@ public class ClientToServerThread implements Runnable {
int sync1;
int sync2;
// TODO: 14/07/17 wmu16 - Work out how to fix this while loop
while(socketOpen) {
while(!socket.isClosed() && socket.isConnected() && socketOpen) {
try {
crcBuffer = new ByteArrayOutputStream();
sync1 = readByte();
@@ -155,24 +139,18 @@ public class ClientToServerThread implements Runnable {
}
}
} else {
clientLog("Packet has been dropped", 1);
logger.warn("Packet has been dropped", 1);
}
}
} catch (ByteReadException e) {
e.printStackTrace();
logger.warn("Byte read exception on ClientToServerThread", 1);
notifyDisconnectListeners("Connection to server was interrupted");
closeSocket();
Platform.runLater(() -> {
Alert alert = new Alert(AlertType.ERROR);
alert.setHeaderText("Host has disconnected");
alert.setContentText("Cannot find Server");
alert.showAndWait();
});
clientLog(e.getMessage(), 1);
return;
}
}
logger.warn("Closed connection to server", 1);
notifyDisconnectListeners("Connection to server was terminated");
closeSocket();
clientLog("Closed connection to Server", 0);
}
public void sendCustomizationRequest(CustomizeRequestType reqType, byte[] payload) {
@@ -181,8 +159,17 @@ public class ClientToServerThread implements Runnable {
os.write(requestMessage.getBuffer());
} catch (IOException e) {
logger.error("Could not send customization request");
notifyDisconnectListeners("Could not communicate with server");
closeSocket();
}
}
private void notifyDisconnectListeners (String message) {
if (socketOpen) {
for (DisconnectedFromHostListener listener : disconnectionListeners) {
listener.notifYDisconnection(message);
}
}
}
/**
@@ -195,7 +182,8 @@ public class ClientToServerThread implements Runnable {
os.write(requestMessage.getBuffer());
} catch (IOException e) {
logger.error("Could not send registration request. Exiting");
System.exit(1);
notifyDisconnectListeners("Failed to register with server");
closeSocket();
}
}
@@ -211,7 +199,6 @@ public class ClientToServerThread implements Runnable {
if (status.equals(RegistrationResponseStatus.SUCCESS_PLAYING)){
clientId = sourceId;
return;
}
@@ -225,11 +212,8 @@ public class ClientToServerThread implements Runnable {
else{
alertErrorText = "Could not connect to server";
}
Platform.runLater(() -> {
new Alert(AlertType.ERROR, alertErrorText, ButtonType.OK).showAndWait();
System.exit(1);
});
notifyDisconnectListeners(alertErrorText);
closeSocket();
}
/**
@@ -260,7 +244,7 @@ public class ClientToServerThread implements Runnable {
new TimerTask() {
@Override
public void run() {
sendBoatAction(new BoatActionMessage(BoatAction.DOWNWIND));
sendBoatActionMessage(new BoatActionMessage(BoatAction.DOWNWIND));
}
}, 0, PACKET_SENDING_INTERVAL_MS
);
@@ -273,14 +257,14 @@ public class ClientToServerThread implements Runnable {
new TimerTask() {
@Override
public void run() {
sendBoatAction(new BoatActionMessage(BoatAction.UPWIND));
sendBoatActionMessage(new BoatActionMessage(BoatAction.UPWIND));
}
}, 0, PACKET_SENDING_INTERVAL_MS
);
}
break;
default:
sendBoatAction(new BoatActionMessage(actionType));
sendBoatActionMessage(new BoatActionMessage(actionType));
break;
}
}
@@ -298,12 +282,14 @@ public class ClientToServerThread implements Runnable {
* Sends a boat action of the given message type.
* @param message The given message type.
*/
private void sendBoatAction(BoatActionMessage message) {
private void sendBoatActionMessage(BoatActionMessage message) {
if (clientId != -1) {
try {
os.write(message.getBuffer());
} catch (IOException e) {
clientLog("Could not write to server", 1);
logger.warn("IOException on attempting to sendBoatAction from Client");
notifyDisconnectListeners("Cannot communicate with server");
closeSocket();
}
}
}
@@ -311,8 +297,9 @@ public class ClientToServerThread implements Runnable {
private void closeSocket() {
try {
socket.close();
socketOpen = false;
} catch (IOException e) {
clientLog("Failed to close the socket", 1);
logger.warn("IOException on attempting to close ClientToServerSocket");
}
}
@@ -332,16 +319,28 @@ public class ClientToServerThread implements Runnable {
listeners.remove(streamListener);
}
public void addDisconnectionListener (DisconnectedFromHostListener listener) {
disconnectionListeners.add(listener);
}
public void removeDisconnectionListener (DisconnectedFromHostListener listener) {
disconnectionListeners.remove(listener);
}
private int readByte() throws ByteReadException {
int currentByte = -1;
try {
currentByte = is.read();
crcBuffer.write(currentByte);
} catch (IOException e) {
clientLog("Read byte failed", 1);
logger.warn("IOException on readByte Client side", 1);
notifyDisconnectListeners("Cannot read from server.");
closeSocket();
}
if (currentByte == -1) {
clientLog("InputStream reach end of stream", 1);
notifyDisconnectListeners("Cannot read from server.");
closeSocket();
logger.warn("InputStream reach end of stream", 1);
}
return currentByte;
}
@@ -360,8 +359,7 @@ public class ClientToServerThread implements Runnable {
}
}
public int getClientId () {
int getClientId () {
return clientId;
}
}
@@ -10,13 +10,14 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import seng302.gameServer.GameState;
import seng302.gameServer.MainServerThread;
import seng302.gameServer.messages.BoatAction;
import seng302.gameServer.messages.BoatStatus;
import seng302.gameServer.messages.BoatAction;
import seng302.model.ClientYacht;
import seng302.model.RaceState;
import seng302.model.stream.packets.StreamPacket;
@@ -70,27 +71,32 @@ public class GameClient {
*/
public void runAsClient(String ipAddress, Integer portNumber) {
try {
socketThread = new ClientToServerThread(ipAddress, portNumber);
startClientToServerThread(ipAddress, portNumber);
socketThread.addDisconnectionListener((cause) -> {
showConnectionError(cause);
Platform.runLater(this::loadStartScreen);
});
socketThread.addStreamObserver(this::parsePackets);
LobbyController lobbyController = loadLobby();
lobbyController.setSocketThread(socketThread);
lobbyController.setPlayerID(socketThread.getClientId());
lobbyController.setPlayerListSource(clientLobbyList);
lobbyController.disableReadyButton();
if (regattaData != null){
lobbyController.setTitle(regattaData.getRegattaName());
lobbyController.setCourseName(regattaData.getCourseName());
}
else{
lobbyController.setTitle(ipAddress);
lobbyController.setCourseName("");
}
lobbyController.addCloseListener((exitCause) -> this.loadStartScreen());
this.lobbyController = lobbyController;
} catch (IOException ioe) {
ioe.printStackTrace();
System.out.println("Unable to connect to host...");
showConnectionError("Unable to find server");
Platform.runLater(this::loadStartScreen);
}
socketThread.addStreamObserver(this::parsePackets);
LobbyController lobbyController = loadLobby();
lobbyController.disableReadyButton();
if (regattaData != null){
lobbyController.setTitle(regattaData.getRegattaName());
lobbyController.setCourseName(regattaData.getCourseName());
}
else{
lobbyController.setTitle(ipAddress);
lobbyController.setCourseName("");
}
lobbyController.addCloseListener((exitCause) -> this.loadStartScreen());
this.lobbyController = lobbyController;
}
/**
@@ -101,36 +107,38 @@ public class GameClient {
public void runAsHost(String ipAddress, Integer portNumber) {
server = new MainServerThread();
try {
socketThread = new ClientToServerThread(ipAddress, portNumber);
} catch (IOException ioe) {
ioe.printStackTrace();
System.out.println("Unable to make local connection to host...");
}
socketThread.addStreamObserver(this::parsePackets);
LobbyController lobbyController = loadLobby();
lobbyController.setPlayerListSource(clientLobbyList);
if (regattaData != null){
lobbyController.setTitle("Hosting: " + regattaData.getRegattaName());
lobbyController.setCourseName(regattaData.getCourseName());
}
else{
lobbyController.setTitle("Hosting: " + ipAddress);
lobbyController.setCourseName("");
}
lobbyController.addCloseListener(exitCause -> {
if (exitCause == CloseStatus.READY) {
GameState.resetStartTime();
lobbyController.disableReadyButton();
server.startGame();
} else if (exitCause == CloseStatus.LEAVE) {
loadStartScreen();
startClientToServerThread(ipAddress, portNumber);
socketThread.addDisconnectionListener((cause) -> {
Platform.runLater(this::loadStartScreen);
});
LobbyController lobbyController = loadLobby();
lobbyController.setSocketThread(socketThread);
lobbyController.setPlayerID(socketThread.getClientId());
lobbyController.setPlayerListSource(clientLobbyList);
if (regattaData != null) {
lobbyController.setTitle("Hosting: " + regattaData.getRegattaName());
lobbyController.setCourseName(regattaData.getCourseName());
} else {
lobbyController.setTitle("Hosting: " + ipAddress);
lobbyController.setCourseName("");
}
});
this.lobbyController = lobbyController;
//server.setGameClient(this);
lobbyController.addCloseListener(exitCause -> {
if (exitCause == CloseStatus.READY) {
GameState.resetStartTime();
lobbyController.disableReadyButton();
server.startGame();
} else if (exitCause == CloseStatus.LEAVE) {
server.terminate();
server = null;
loadStartScreen();
}
});
this.lobbyController = lobbyController;
} catch (IOException ioe) {
showConnectionError("Cannot connect to server as host");
Platform.runLater(this::loadStartScreen);
}
}
private void loadStartScreen() {
@@ -145,10 +153,24 @@ public class GameClient {
holderPane.getChildren().clear();
holderPane.getChildren().add(fxmlLoader.load());
} catch (IOException e) {
e.printStackTrace();
showConnectionError("JavaFX crashed. Please restart the app");
}
}
private void showConnectionError (String message) {
Platform.runLater(() -> {
Alert alert = new Alert(AlertType.ERROR);
alert.setHeaderText("Connection Error");
alert.setContentText(message);
alert.showAndWait();
});
}
private void startClientToServerThread (String ipAddress, int portNumber) throws IOException {
socketThread = new ClientToServerThread(ipAddress, portNumber);
socketThread.addStreamObserver(this::parsePackets);
}
/**
* Loads a view of the lobby into the clients pane
*
@@ -163,12 +185,7 @@ public class GameClient {
} catch (IOException e) {
e.printStackTrace();
}
LobbyController lobbyController = fxmlLoader.getController();
lobbyController.setSocketThread(socketThread);
lobbyController.setPlayerListSource(clientLobbyList);
lobbyController.setPlayerID(socketThread.getClientId());
return lobbyController;
return fxmlLoader.getController();
}
private void loadRaceView() {
+36 -24
View File
@@ -278,7 +278,31 @@ public class GameView extends Pane {
colour = Color.BLACK;
}
//Creating mark arrows.
createMarkArrows(sequence);
//Scale race to markers if there is no border.
if (borderPoints == null) {
rescaleRace(new ArrayList<>(markerObjects.keySet()));
}
//Move the Markers to initial position.
markerObjects.forEach(((mark, marker) -> {
Point2D p2d = findScaledXY(mark.getLat(), mark.getLng());
marker.setLayoutX(p2d.getX());
marker.setLayoutY(p2d.getY());
}));
Platform.runLater(() -> {
markers.getChildren().clear();
markers.getChildren().addAll(gates);
markers.getChildren().addAll(markerObjects.values());
});
}
/**
* Calculates all the data needed for to create mark arrows. Requires that a course has been
* added to the gameview.
* @param sequence The order in which marks are traversed.
*/
private void createMarkArrows (List<Corner> sequence) {
for (int i=1; i < sequence.size()-1; i++) { //General case.
double averageLat = 0;
double averageLng = 0;
@@ -298,7 +322,7 @@ public class GameView extends Pane {
averageLng += mark.getLng();
}
GeoPoint nextMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks);
// TODO: 16/08/17 This comparison is cancer and deserves to die.
// TODO: 16/08/17 This comparison doesn't need to exist but the alternative is to user server enum client side.
for (Mark mark : course.get(i).getMarks()) {
markerObjects.get(mark).addArrows(
mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT,
@@ -307,9 +331,11 @@ public class GameView extends Pane {
);
}
}
createStartLineArrows();
createFinishLineArrows();
}
// TODO: 16/08/17 Make this cleaner
//First mark case
private void createStartLineArrows () {
double averageLat = 0;
double averageLng = 0;
int numMarks = 0;
@@ -326,10 +352,12 @@ public class GameView extends Pane {
GeoUtility.getBearing(mark, firstMarkAv)
);
}
//Last Mark case
numMarks = 0;
averageLat = 0;
averageLng = 0;
}
private void createFinishLineArrows () {
double numMarks = 0;
double averageLat = 0;
double averageLng = 0;
for (Mark mark : course.get(course.size()-2).getMarks()) {
numMarks += 1;
averageLat += mark.getLat();
@@ -343,22 +371,6 @@ public class GameView extends Pane {
GeoUtility.getBearing(mark, mark)
);
}
//Scale race to markers if there is no border.
if (borderPoints == null) {
rescaleRace(new ArrayList<>(markerObjects.keySet()));
}
//Move the Markers to initial position.
markerObjects.forEach(((mark, marker) -> {
Point2D p2d = findScaledXY(mark.getLat(), mark.getLng());
marker.setLayoutX(p2d.getX());
marker.setLayoutY(p2d.getY());
}));
Platform.runLater(() -> {
markers.getChildren().clear();
markers.getChildren().addAll(gates);
markers.getChildren().addAll(markerObjects.values());
});
}
/**
@@ -49,17 +49,13 @@ public class Marker extends Group {
*/
public void addArrows(MarkArrowFactory.RoundingSide roundingSide, double entryAngle,
double exitAngle) {
//Change Color.GRAY to this.colour to revert all gray arrows.
enterArrows.add(
MarkArrowFactory.constructEntryArrow(roundingSide, entryAngle, exitAngle, colour)
MarkArrowFactory.constructEntryArrow(roundingSide, entryAngle, exitAngle, Color.GRAY)
);
exitArrows.add(
MarkArrowFactory.constructExitArrow(roundingSide, exitAngle, colour)
MarkArrowFactory.constructExitArrow(roundingSide, exitAngle, Color.GRAY)
);
// Platform.runLater(() -> {
// this.getChildren().add(enterArrows.get(enterArrows.size()-1));
// this.getChildren().add(exitArrows.get(exitArrows.size()-1));
// });
}
/**