diff --git a/src/main/java/seng302/gameServer/GameState.java b/src/main/java/seng302/gameServer/GameState.java index 80ecac3e..aaea82cf 100644 --- a/src/main/java/seng302/gameServer/GameState.java +++ b/src/main/java/seng302/gameServer/GameState.java @@ -15,6 +15,7 @@ import org.w3c.dom.Document; import org.xml.sax.InputSource; import seng302.gameServer.messages.BoatAction; import seng302.gameServer.messages.BoatStatus; +import seng302.gameServer.messages.ChatterMessage; import seng302.gameServer.messages.CustomizeRequestType; import seng302.gameServer.messages.MarkRoundingMessage; import seng302.gameServer.messages.MarkType; @@ -41,7 +42,6 @@ public class GameState implements Runnable { @FunctionalInterface interface NewMessageListener { - void notify(Message message); } @@ -59,6 +59,7 @@ public class GameState implements Runnable { private static Long previousUpdateTime; public static Double windDirection; private static Double windSpeed; + private static Double speedMultiplier = 1d; private static Boolean customizationFlag; // dirty flag to tell if a player has customized their boat. @@ -72,19 +73,9 @@ public class GameState implements Runnable { private static Set marks; private static List courseLimit; - private static List markListeners; + private static List messageListeners; private static Map playerStringMap = new HashMap<>(); - /* - Ideally I would like to make this class an object instantiated by the server and given to - it's created threads if necessary. Outside of that I think the dependencies on it - (atm only Yacht & GameClient) can be removed from most other classes. The observable list of - players could be pulled directly from the server by the GameClient since it instantiates it - and it is reasonable for it to pull data. The current setup of publicly available statics is - pretty meh IMO because anything can change it making it unreliable and like people did with - the old ServerParser class everything that needs shared just gets thrown in the static - collections and things become a real mess. - */ public GameState(String hostIpAddress) { windDirection = 180d; @@ -100,7 +91,7 @@ public class GameState implements Runnable { //set this when game stage changes to prerace previousUpdateTime = System.currentTimeMillis(); markOrder = new MarkOrder(); //This could be instantiated at some point with a select map? - markListeners = new ArrayList<>(); + messageListeners = new ArrayList<>(); resetStartTime(); @@ -366,7 +357,7 @@ public class GameState implements Runnable { Double velocity = yacht.getCurrentVelocity(); Double trueWindAngle = Math.abs(windDirection - yacht.getHeading()); Double boatSpeedInKnots = PolarTable.getBoatSpeed(getWindSpeedKnots(), trueWindAngle); - Double maxBoatSpeed = GeoUtility.knotsToMMS(boatSpeedInKnots) * 5; + Double maxBoatSpeed = GeoUtility.knotsToMMS(boatSpeedInKnots) * speedMultiplier; // TODO: 15/08/17 remove magic numbers from these equations. if (yacht.getSailIn()) { if (velocity < maxBoatSpeed - 500) { @@ -671,8 +662,8 @@ public class GameState implements Runnable { } private static void notifyMessageListeners(Message message) { - for (NewMessageListener mpl : markListeners) { - mpl.notify(message); + for (NewMessageListener ml : messageListeners) { + ml.notify(message); } } @@ -685,7 +676,7 @@ public class GameState implements Runnable { public static void addMarkPassListener(NewMessageListener listener) { - markListeners.add(listener); + messageListeners.add(listener); } public static void setCustomizationFlag() { @@ -699,4 +690,21 @@ public class GameState implements Runnable { public static void resetCustomizationFlag() { customizationFlag = false; } + + public static void broadcastChatter(ChatterMessage chatterMessage) { + notifyMessageListeners(chatterMessage); + } + + public static void endRace () { + yachts.forEach((id, yacht) -> yacht.setBoatStatus(BoatStatus.FINISHED)); + currentStage = GameStages.FINISHED; + } + + public static void setSpeedMultiplier (double multiplier) { + speedMultiplier = multiplier; + } + + public static double getSpeedMultiplier () { + return speedMultiplier; + } } diff --git a/src/main/java/seng302/gameServer/HeartbeatThread.java b/src/main/java/seng302/gameServer/HeartbeatThread.java index b9367134..970a7ef4 100644 --- a/src/main/java/seng302/gameServer/HeartbeatThread.java +++ b/src/main/java/seng302/gameServer/HeartbeatThread.java @@ -44,20 +44,23 @@ public class HeartbeatThread implements Runnable { * The delegate is notified if a player has disconnected */ private void sendHeartbeatToAllPlayers(){ - Message heartbeat = new Heartbeat(seqNum); - for (Player player : GameState.getPlayers()){ - if (!player.getSocket().isConnected()) { - playerLostConnection(player); - } - - try { - player.getSocket().getOutputStream().write(heartbeat.getBuffer()); - } catch (IOException e) { - playerLostConnection(player); + try { + Message heartbeat = new Heartbeat(seqNum); + for (Player player : GameState.getPlayers()) { + if (!player.getSocket().isConnected()) { + playerLostConnection(player); + } + try { + player.getSocket().getOutputStream().write(heartbeat.getBuffer()); + } catch (IOException e) { + playerLostConnection(player); + } } + updateDelegate(); + seqNum++; + } catch (NullPointerException ne) { + // TODO: 4/09/17 Just ignoring this at the moment. Caused by players getting removed elsewhere. } - updateDelegate(); - seqNum++; } /** diff --git a/src/main/java/seng302/gameServer/MainServerThread.java b/src/main/java/seng302/gameServer/MainServerThread.java index 83fb304c..45a88185 100644 --- a/src/main/java/seng302/gameServer/MainServerThread.java +++ b/src/main/java/seng302/gameServer/MainServerThread.java @@ -86,17 +86,20 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate { //FINISHED else if (GameState.getCurrentStage() == GameStages.FINISHED) { - terminate(); + broadcastMessage(makeRaceStatusMessage()); + try { + Thread.sleep(1000); //Hackish fix to make sure all threads have sent closing RaceStatus + terminate(); + } catch (InterruptedException ie) { + serverLog("Thread interrupted while waiting to terminate clients", 1); + } } } - - // TODO: 14/07/17 wmu16 - Send out disconnect packet to clients try { for (ServerToClientThread serverToClientThread : serverToClientThreads) { serverToClientThread.terminate(); } serverSocket.close(); - return; } catch (IOException e) { System.out.println("IO error in server thread handler upon closing socket"); } @@ -169,6 +172,9 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate { @Override public void clientConnected(ServerToClientThread serverToClientThread) { serverLog("Player Connected From " + serverToClientThread.getThread().getName(), 0); + if (serverToClientThreads.size() == 0) { //Sets first client as host. + serverToClientThread.setAsHost(); + } serverToClientThreads.add(serverToClientThread); serverToClientThread.addConnectionListener(() -> { for (ServerToClientThread thread : serverToClientThreads) { @@ -257,6 +263,8 @@ public class MainServerThread implements Runnable, ClientConnectionDelegate { if (timeTillStart > PREPATORY_TIME) { raceStatus = RaceStatus.PREPARATORY; } + } else if (GameState.getCurrentStage() == GameStages.FINISHED) { + raceStatus = RaceStatus.TERMINATED; } else { raceStatus = RaceStatus.STARTED; } diff --git a/src/main/java/seng302/gameServer/ServerPacketParser.java b/src/main/java/seng302/gameServer/ServerPacketParser.java index 62d971ea..f555d62f 100644 --- a/src/main/java/seng302/gameServer/ServerPacketParser.java +++ b/src/main/java/seng302/gameServer/ServerPacketParser.java @@ -2,6 +2,7 @@ package seng302.gameServer; import java.util.Arrays; import seng302.gameServer.messages.BoatAction; +import seng302.gameServer.messages.ChatterMessage; import seng302.gameServer.messages.ClientType; import seng302.gameServer.messages.CustomizeRequestType; import seng302.gameServer.messages.Message; @@ -28,5 +29,11 @@ public class ServerPacketParser { long type = Message.bytesToLong(Arrays.copyOfRange(payload, 4, 5)); return CustomizeRequestType.getRequestType((int) type); } + + public static ChatterMessage extractChatterText(byte[] payload) { + return new ChatterMessage( + payload[1], new String(Arrays.copyOfRange(payload, 3, payload.length)) + ); + } } diff --git a/src/main/java/seng302/gameServer/ServerToClientThread.java b/src/main/java/seng302/gameServer/ServerToClientThread.java index 3a9b4057..2b447cf1 100644 --- a/src/main/java/seng302/gameServer/ServerToClientThread.java +++ b/src/main/java/seng302/gameServer/ServerToClientThread.java @@ -22,6 +22,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import seng302.gameServer.messages.BoatAction; import seng302.gameServer.messages.BoatLocationMessage; +import seng302.gameServer.messages.ChatterMessage; import seng302.gameServer.messages.ClientType; import seng302.gameServer.messages.CustomizeRequestType; import seng302.gameServer.messages.Message; @@ -30,23 +31,6 @@ import seng302.gameServer.messages.RegistrationResponseStatus; import seng302.gameServer.messages.XMLMessage; import seng302.gameServer.messages.XMLMessageSubType; import seng302.gameServer.messages.YachtEventCodeMessage; -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; -import seng302.gameServer.messages.BoatAction; -import seng302.gameServer.messages.BoatLocationMessage; -import seng302.gameServer.messages.ClientType; -import seng302.gameServer.messages.Message; -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; @@ -91,6 +75,7 @@ public class ServerToClientThread implements Runnable, Observer { private ClientType clientType; private Boolean isRegistered = false; + private Boolean isHost = false; private XMLGenerator xml; @@ -225,7 +210,12 @@ public class ServerToClientThread implements Runnable, Observer { completeRegistration(requestedType); break; - + case CHATTER_TEXT: +// GameState.broadcastChatter( +// ServerPacketParser.extractChatterText(payload) +// ); + parseChatter(payload); + break; case RACE_CUSTOMIZATION_REQUEST: Long sourceID = Message .bytesToLong(Arrays.copyOfRange(payload, 0, 3)); @@ -386,4 +376,40 @@ public class ServerToClientThread implements Runnable, Observer { public void addDisconnectListener(DisconnectListener disconnectListener) { this.disconnectListener = disconnectListener; } + + public void setAsHost() { + isHost = true; + } + + private void parseChatter(byte[] chatterPayload) { + String chatterText = new String( + Arrays.copyOfRange(chatterPayload, 3, 3 + chatterPayload.length) + ); + String[] words = chatterText.split("\\s+"); + if (words.length > 2 && isHost) { + switch (words[2].trim()) { + case ">speed": + try { + GameState.setSpeedMultiplier(Double.valueOf(words[3])); + GameState.broadcastChatter(new ChatterMessage( + Byte.toUnsignedInt(chatterPayload[1]), + "SERVER: Speed modifier set to x" + words[3] + )); + } catch (Exception e) { + logger.error("cannot parse >speed value"); + } + return; + case ">finish": + GameState.broadcastChatter(new ChatterMessage( + chatterPayload[1], + "SERVER: Game will now finish" + )); + GameState.endRace(); + return; + } + } + GameState.broadcastChatter( + ServerPacketParser.extractChatterText(chatterPayload) + ); + } } diff --git a/src/main/java/seng302/gameServer/messages/ChatterMessage.java b/src/main/java/seng302/gameServer/messages/ChatterMessage.java index f312109f..42f17093 100644 --- a/src/main/java/seng302/gameServer/messages/ChatterMessage.java +++ b/src/main/java/seng302/gameServer/messages/ChatterMessage.java @@ -11,9 +11,11 @@ public class ChatterMessage extends Message { private int message_size = 21; private String message; - public ChatterMessage(int message_type, int message_size, String message) { + public ChatterMessage(int message_type, String message) { + byte[] byteMessage = message.getBytes(); + this.message_type = message_type; - this.message_size = message_size; + this.message_size = byteMessage.length; this.message = message; setHeader(new Header(MessageType.CHATTER_TEXT, 1, (short) getSize())); @@ -23,7 +25,7 @@ public class ChatterMessage extends Message { putByte((byte) MESSAGE_VERSION_NUMBER); putInt(message_type, 1); putInt(message_size, 1); - putBytes(message.getBytes()); + putBytes(byteMessage); writeCRC(); rewind(); diff --git a/src/main/java/seng302/model/stream/parser/RaceStatusData.java b/src/main/java/seng302/model/stream/parser/RaceStatusData.java index ba836442..867ff282 100644 --- a/src/main/java/seng302/model/stream/parser/RaceStatusData.java +++ b/src/main/java/seng302/model/stream/parser/RaceStatusData.java @@ -57,7 +57,7 @@ public class RaceStatusData { * Returns the data for boats collected form race status packets. * * @return A list of boat data. Boat data is in the form - * [boatID, estTimeToNextMark, estTimeToFinish, legNumber]. + * [boatID, estTimeToNextMark, estTimeToFinish, legNumber, status]. */ public List getBoatData () { return boatData; diff --git a/src/main/java/seng302/utilities/StreamParser.java b/src/main/java/seng302/utilities/StreamParser.java index 1f90eac8..748337df 100644 --- a/src/main/java/seng302/utilities/StreamParser.java +++ b/src/main/java/seng302/utilities/StreamParser.java @@ -2,9 +2,11 @@ package seng302.utilities; import java.io.IOException; import java.io.StringReader; +import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javafx.util.Pair; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -62,31 +64,10 @@ public class StreamParser { long windDir = bytesToLong(Arrays.copyOfRange(payload, 18, 20)); long rawWindSpeed = bytesToLong(Arrays.copyOfRange(payload, 20, 22)); -// DateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); -// currentTime = format.format((new Date(currentTime))) - RaceStatusData data = new RaceStatusData( windDir, rawWindSpeed, raceStatus, currentTime, expectedStartTime ); -// long timeTillStart = -// ((new Date(expectedStartTime)).getTime() - (new Date(currentTime)).getTime()) / 1000; -// -// if (timeTillStart > 0) { -// timeSinceStart = timeTillStart; -// } else { -// if (raceStatus == 4 || raceStatus == 8) { -// raceFinished = true; -// raceStarted = false; -// } else if (!raceStarted) { -// raceStarted = true; -// raceFinished = false; -// } -// timeSinceStart = timeTillStart; -// } -// - -// int noBoats = payload[22]; int raceType = payload[23]; long boatID, estTimeAtNextMark, estTimeAtFinish; @@ -106,24 +87,6 @@ public class StreamParser { return data; } -// private static void setBoatLegPosition(Yacht updatingBoat, Integer leg){ -// Integer placing = 1; -// if (leg != updatingBoat.getLegNumber() && (raceStarted || raceFinished)) { -// for (Yacht boat : boats.values()) { -// if (boat.getLegNumber() != null && leg <= boat.getLegNumber()){ -// placing += 1; -// } -// } -// updatingBoat.setPlacing(placing.toString()); -// updatingBoat.setLegNumber(leg); -// boatsPos.putIfAbsent(placing, updatingBoat); -// boatsPos.replace(placing, updatingBoat); -// } else if(updatingBoat.getLegNumber() == null){ -// updatingBoat.setPlacing("1"); -// updatingBoat.setLegNumber(leg); -// } -// } - /** * Parses and returns the text from a StreamPacket containing text data for display. * @@ -255,15 +218,15 @@ public class StreamParser { * @return Chatter text message as a string. Returns null if the packet is not of type * CHATTER_TEXT. */ - public static String extractChatterText(StreamPacket packet) { + public static Pair extractChatterText(StreamPacket packet) { if (packet.getType() != PacketType.CHATTER_TEXT) { return null; } byte[] payload = packet.getPayload(); int messageVersionNo = payload[0]; int messageType = payload[1]; - int length = payload[2]; - return new String(Arrays.copyOfRange(payload, 3, 3 + length)); + int length = (int) bytesToLong(new byte[]{payload[2]}); + return new Pair<>(messageType, new String(Arrays.copyOfRange(payload, 3, 3 + length))); } /** @@ -392,26 +355,6 @@ public class StreamParser { }; } - - public static void extractBoatAction(StreamPacket packet) { - byte[] payload = packet.getPayload(); - int messageVersionNo = payload[0]; - long actionType = bytesToLong(Arrays.copyOfRange(payload, 0, 1)); - if (actionType == 1) { - System.out.println("VMG"); - } else if (actionType == 2) { - System.out.println("SAILS IN"); - } else if (actionType == 3) { - System.out.println("SAILS OUT"); - } else if (actionType == 4) { - System.out.println("TACK/GYBE"); - } else if (actionType == 5) { - System.out.println("UPWIND"); - } else if (actionType == 6) { - System.out.println("DOWNWIND"); - } - } - /** * takes an array of up to 7 bytes and returns a positive long constructed from the input bytes * diff --git a/src/main/java/seng302/visualiser/ClientToServerThread.java b/src/main/java/seng302/visualiser/ClientToServerThread.java index 57256760..f84fc421 100644 --- a/src/main/java/seng302/visualiser/ClientToServerThread.java +++ b/src/main/java/seng302/visualiser/ClientToServerThread.java @@ -18,6 +18,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import seng302.gameServer.messages.BoatAction; import seng302.gameServer.messages.BoatActionMessage; +import seng302.gameServer.messages.ChatterMessage; import seng302.gameServer.messages.ClientType; import seng302.gameServer.messages.CustomizeRequestMessage; import seng302.gameServer.messages.CustomizeRequestType; @@ -283,9 +284,17 @@ public class ClientToServerThread implements Runnable { * @param message The given message type. */ private void sendBoatActionMessage(BoatActionMessage message) { + sendByteBuffer(message.getBuffer()); + } + + public void sendChatterMessage(String message) { + sendByteBuffer(new ChatterMessage(clientId, message).getBuffer()); + } + + private void sendByteBuffer(byte[] bytes) { if (clientId != -1) { try { - os.write(message.getBuffer()); + os.write(bytes); } catch (IOException e) { logger.warn("IOException on attempting to sendBoatAction from Client"); notifyDisconnectListeners("Cannot communicate with server"); @@ -294,7 +303,7 @@ public class ClientToServerThread implements Runnable { } } - private void closeSocket() { + public void closeSocket() { try { socket.close(); socketOpen = false; diff --git a/src/main/java/seng302/visualiser/GameClient.java b/src/main/java/seng302/visualiser/GameClient.java index b647fa97..81284a06 100644 --- a/src/main/java/seng302/visualiser/GameClient.java +++ b/src/main/java/seng302/visualiser/GameClient.java @@ -1,9 +1,11 @@ package seng302.visualiser; import java.io.IOException; +import java.text.SimpleDateFormat; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Date; import java.util.Map; import java.util.TimeZone; import javafx.application.Platform; @@ -13,8 +15,10 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; +import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Pane; +import javafx.util.Pair; import seng302.gameServer.GameState; import seng302.gameServer.MainServerThread; import seng302.gameServer.messages.BoatAction; @@ -78,7 +82,6 @@ public class GameClient { startClientToServerThread(ipAddress, portNumber); socketThread.addDisconnectionListener((cause) -> { showConnectionError(cause); - tearDownConnection(); Platform.runLater(this::loadStartScreen); }); socketThread.addStreamObserver(this::parsePackets); @@ -96,10 +99,7 @@ public class GameClient { lobbyController.setCourseName(""); } - lobbyController.addCloseListener((exitCause) -> { - this.tearDownConnection(); - this.loadStartScreen(); - }); + lobbyController.addCloseListener((exitCause) -> this.loadStartScreen()); this.lobbyController = lobbyController; } catch (IOException ioe) { showConnectionError("Unable to find server"); @@ -117,7 +117,6 @@ public class GameClient { try { startClientToServerThread(ipAddress, portNumber); socketThread.addDisconnectionListener((cause) -> { - this.tearDownConnection(); Platform.runLater(this::loadStartScreen); }); LobbyController lobbyController = loadLobby(); @@ -138,7 +137,8 @@ public class GameClient { lobbyController.disableReadyButton(); server.startGame(); } else if (exitCause == CloseStatus.LEAVE) { - tearDownConnection(); + server.terminate(); + server = null; loadStartScreen(); } }); @@ -149,21 +149,10 @@ public class GameClient { } } - private void tearDownConnection() { - socketThread.setSocketToClose(); - if (server != null) { - server.terminate(); - server = null; - } - } - private void loadStartScreen() { -// socketThread.setSocketToClose(); -// if (server != null) { -// server.terminate(); -// server = null; -// } - + if (socketThread != null) { + socketThread.setSocketToClose(); + } Sounds.stopMusic(); Sounds.playMenuMusic(); FXMLLoader fxmlLoader = new FXMLLoader( @@ -214,8 +203,15 @@ public class GameClient { raceView = fxmlLoader.getController(); ClientYacht player = allBoatsMap.get(socketThread.getClientId()); raceView.loadRace(allBoatsMap, courseData, raceState, player); + raceView.getSendPressedProperty().addListener((obs, old, isPressed) -> { + if (isPressed) { + formatAndSendChatMessage(raceView.readChatInput()); + } + }); } + + private void loadFinishScreenView() { FXMLLoader fxmlLoader = loadFXMLToHolder("/views/FinishScreenView.fxml"); FinishScreenViewController controller = fxmlLoader.getController(); @@ -299,6 +295,14 @@ public class GameClient { case YACHT_EVENT_CODE: showCollisionAlert(StreamParser.extractYachtEventCode(packet)); break; + + case CHATTER_TEXT: + Pair playerIdMessagePair = StreamParser + .extractChatterText(packet); + raceView.updateChatHistory( + allBoatsMap.get(playerIdMessagePair.getKey()).getColour(), + playerIdMessagePair.getValue() + ); } } } @@ -396,6 +400,12 @@ public class GameClient { * @param e The key event triggering this call */ private void keyPressed(KeyEvent e) { + if (raceView.isChatInputFocused()) { + if (e.getCode() == KeyCode.ENTER) { + formatAndSendChatMessage(raceView.readChatInput()); + } + return; + } switch (e.getCode()) { case SPACE: // align with vmg socketThread.sendBoatAction(BoatAction.VMG); break; @@ -404,12 +414,16 @@ public class GameClient { case PAGE_DOWN: // downwind socketThread.sendBoatAction(BoatAction.DOWNWIND); break; case ENTER: // tack/gybe + // if chat box is active take whatever is in there and send it to server socketThread.sendBoatAction(BoatAction.TACK_GYBE); break; } } private void keyReleased(KeyEvent e) { + if (raceView.isChatInputFocused()) { + return; + } switch (e.getCode()) { //TODO 12/07/17 Determine the sail state and send the appropriate packet (eg. if sails are in, send a sail out packet) case SHIFT: // sails in/sails out @@ -440,4 +454,19 @@ public class GameClient { ); } } + + private void formatAndSendChatMessage(String rawChat) { + if (rawChat.length() > 0) { + socketThread.sendChatterMessage( + new SimpleDateFormat("[HH:mm:ss] ").format(new Date()) + + allBoatsMap.get(socketThread.getClientId()).getShortName() + ": " + rawChat + ); + } + } + + + public ClientToServerThread getSocketThread() { + return socketThread; + } + } diff --git a/src/main/java/seng302/visualiser/GameView.java b/src/main/java/seng302/visualiser/GameView.java index 76c8197a..61441b72 100644 --- a/src/main/java/seng302/visualiser/GameView.java +++ b/src/main/java/seng302/visualiser/GameView.java @@ -65,6 +65,7 @@ public class GameView extends Pane { private double metersPerPixelX, metersPerPixelY; final double SCALE_DELTA = 1.1; + private boolean isZoom = false; private Text fpsDisplay = new Text(); private Polygon raceBorder = new CourseBoundary(); @@ -102,7 +103,7 @@ public class GameView extends Pane { private void zoomOut() { scaleFactor = 0.1; - if (this.getScaleX() > 0.5) { + if (this.isZoom && this.getScaleX() > 0.5) { this.setScaleX(this.getScaleX() - scaleFactor); this.setScaleY(this.getScaleY() - scaleFactor); } @@ -110,7 +111,7 @@ public class GameView extends Pane { private void zoomIn() { scaleFactor = 0.10; - if (this.getScaleX() < 2.5) { + if (this.isZoom && this.getScaleX() < 2.5) { this.setScaleX(this.getScaleX() + scaleFactor); this.setScaleY(this.getScaleY() + scaleFactor); } @@ -143,6 +144,13 @@ public class GameView extends Pane { gameObjects.add(raceBorder); gameObjects.add(markers); initializeTimer(); + this.sceneProperty().addListener(((observable, oldValue, scene) -> { + if (scene != null) { + setupZoom(); + } else { + disableZoom(); + } + })); } private void initializeTimer() { @@ -440,17 +448,25 @@ public class GameView extends Pane { /** * Enables zoom. Has to be called after this is added to a scene. */ - public void enableZoom () { - if (this.getScene() != null) { - this.getScene().addEventHandler(KeyEvent.KEY_PRESSED, (event) -> { - if (event.getCode() == KeyCode.Z) { - zoomIn(); - } else if (event.getCode() == KeyCode.X) { - zoomOut(); - } - }); - } + private void setupZoom() { + this.getScene().addEventHandler(KeyEvent.KEY_PRESSED, (event) -> { + if (event.getCode() == KeyCode.Z) { + zoomIn(); + } else if (event.getCode() == KeyCode.X) { + zoomOut(); + } + }); + enableZoom(); } + + public void enableZoom() { + isZoom = true; + } + + public void disableZoom() { + isZoom = false; + } + /** * Rescales the race to the size of the window. * diff --git a/src/main/java/seng302/visualiser/controllers/RaceViewController.java b/src/main/java/seng302/visualiser/controllers/RaceViewController.java index c4835bc6..300722cd 100644 --- a/src/main/java/seng302/visualiser/controllers/RaceViewController.java +++ b/src/main/java/seng302/visualiser/controllers/RaceViewController.java @@ -9,6 +9,7 @@ import java.util.TimerTask; import java.util.concurrent.TimeUnit; import javafx.animation.Timeline; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -25,6 +26,7 @@ import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Slider; +import javafx.scene.control.TextField; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; @@ -48,12 +50,23 @@ import seng302.visualiser.controllers.annotations.ImportantAnnotationController; import seng302.visualiser.controllers.annotations.ImportantAnnotationDelegate; import seng302.visualiser.controllers.annotations.ImportantAnnotationsState; import seng302.visualiser.fxObjects.BoatObject; +import seng302.visualiser.fxObjects.ChatHistory; /** * Controller class that manages the display of a race */ public class RaceViewController extends Thread implements ImportantAnnotationDelegate { + private final int CHAT_LIMIT = 128; + + @FXML + private Pane basePane; + @FXML + private Button chatSend; + @FXML + private Pane chatHistoryHolder; + @FXML + private TextField chatInput; @FXML private LineChart raceSparkLine; @FXML @@ -86,6 +99,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel private GameView gameView; private RaceState raceState; + private ChatHistory chatHistory; + private Timeline timerTimeline; private Timer timer = new Timer(); private List> sparkLineData = new ArrayList<>(); @@ -104,10 +119,27 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel raceSparkLine.visibleProperty().setValue(false); raceSparkLine.getYAxis().setAutoRanging(false); sparklineYAxis.setTickMarkVisible(false); - positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString()); selectAnnotationBtn.setOnAction(event -> loadSelectAnnotationView()); + chatInput.lengthProperty().addListener((obs, oldLen, newLen) -> { + if (newLen.intValue() > CHAT_LIMIT) { + chatInput.setText(chatInput.getText().substring(0, CHAT_LIMIT)); + } + }); + chatHistory = new ChatHistory(); + chatHistoryHolder.getChildren().addAll(chatHistory); + chatHistory.prefWidthProperty().bind( + chatHistoryHolder.widthProperty() + ); + chatHistory.prefHeightProperty().bind( + chatHistoryHolder.heightProperty() + ); +// chatHistory.setFitToWidth(true); +// chatHistory.setFitToHeight(true); +// chatHistory.textProperty().addListener((obs, oldValue, newValue) -> { +// chatHistory.setScrollTop(Double.MAX_VALUE); +// }); } public void loadRace ( @@ -119,12 +151,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel this.markers = raceData.getCompoundMarks(); this.raceState = raceState; - initializeUpdateTimer(); - initialiseFPSCheckBox(); - initialiseAnnotationSlider(); - initialiseBoatSelectionComboBox(); - initialiseSparkLine(); - raceState.getPlayerPositions().addListener((ListChangeListener) c -> { while (c.next()) { if (c.wasPermutated()) { @@ -143,7 +169,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel gameView.updateCourse( new ArrayList<>(raceData.getCompoundMarks().values()), raceData.getMarkSequence() ); - gameView.enableZoom(); gameView.setBoatAsPlayer(player); gameView.startRace(); @@ -158,6 +183,19 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel updateWindDirection(raceState.windDirectionProperty().doubleValue()); updateWindSpeed(raceState.getWindSpeed()); gameView.setWindDir(raceState.windDirectionProperty().doubleValue()); + chatInput.focusedProperty().addListener((obs, oldValue, newValue) -> { + if (newValue) { + gameView.disableZoom(); + } else { + gameView.enableZoom(); + } + }); + + initializeUpdateTimer(); + initialiseFPSCheckBox(); + initialiseAnnotationSlider(); + initialiseBoatSelectionComboBox(); + initialiseSparkLine(); } /** @@ -539,7 +577,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel yachtSelectionComboBox.setItems( FXCollections.observableArrayList(participants.values()) ); - //Null check is if the listener is fired but nothing selected yachtSelectionComboBox.valueProperty().addListener((obs, lastSelection, selectedBoat) -> { if (selectedBoat != null) { gameView.selectBoat(selectedBoat); @@ -625,4 +662,24 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel this.courseData = raceData; gameView.updateBorder(raceData.getCourseLimit()); } + + public ReadOnlyBooleanProperty getSendPressedProperty() { + return chatSend.pressedProperty(); + } + + public boolean isChatInputFocused() { + return chatInput.focusedProperty().getValue(); + } + + public String readChatInput() { + String chat = chatInput.getText(); + chatInput.clear(); + basePane.requestFocus(); + return chat; + } + + public void updateChatHistory(Paint playerColour, String newMessage) { + Platform.runLater(() -> chatHistory.addMessage(playerColour, newMessage)); + } + } \ No newline at end of file diff --git a/src/main/java/seng302/visualiser/controllers/StartScreenController.java b/src/main/java/seng302/visualiser/controllers/StartScreenController.java index 85faceb0..5cdf36a2 100644 --- a/src/main/java/seng302/visualiser/controllers/StartScreenController.java +++ b/src/main/java/seng302/visualiser/controllers/StartScreenController.java @@ -31,15 +31,12 @@ public class StartScreenController implements Initializable { @FXML private TextField ipTextField; @FXML - private TextField portTextField; - @FXML - private GridPane startScreen2; - @FXML private AnchorPane holder; - GameClient gameClient; + private GameClient gameClient; public void initialize(URL url, ResourceBundle resourceBundle) { + if (Sounds.isMusicMuted()) { muteMusicButton.setText("UnMute Music"); } else { @@ -53,73 +50,19 @@ public class StartScreenController implements Initializable { // gameClient = new GameClient(holder); } -// -// /** -// * Loads the fxml content into the parent pane -// * @param jfxUrl -// * @return the controller of the fxml -// */ -// private Object setContentPane(String jfxUrl) { -// try { -// AnchorPane contentPane = (AnchorPane) startScreen2.getParent(); -// contentPane.getChildren().removeAll(); -// contentPane.getChildren().clear(); -// contentPane.getStylesheets().add(getClass().getResource("/css/master.css").toString()); -// FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(jfxUrl)); -// contentPane.getChildren().addAll((Pane) fxmlLoader.load()); -// -// return fxmlLoader.getController(); -// } catch (IOException e) { -// e.printStackTrace(); -// } -// return null; -// } - /** - * 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. - * Starts a client to server thread and connects to own ip. - * Switches to the lobby screen + * Creates an instance of GameClient and runs it as a host. */ @FXML public void hostButtonPressed() { Sounds.playButtonClick(); -// new GameState(getLocalHostIp()); gameClient = new GameClient(holder); gameClient.runAsHost(getLocalHostIp(), 4942); -// try { -//// String ipAddress = InetAddress.getLocalHost().getHostAddress(); -//// new GameState(ipAddress); -//// new MainServerThread(); -//// ClientToServerThread clientToServerThread = new ClientToServerThread("localhost", 4950); -//// controller.setClientToServerThread(clientToServerThread); -// // get the lobby controller so that we can pass the game server thread to it -// new GameState(getLocalHostIp()); -// MainServerThread mainServerThread = new MainServerThread(); -//// ClientState.setHost(true); -// // host will connect and handshake to itself after setting up the server -// // TODO: 24/07/17 wmu16 - Make port number some static global type constant? -//// ClientToServerThread clientToServerThread = new ClientToServerThread(ClientState.getHostIp(), 4942); -//// ClientState.setConnectedToHost(true); -//// controller.setClientToServerThread(clientToServerThread); -// LobbyController lobbyController = (LobbyController) setContentPane("/views/LobbyView.fxml"); -// lobbyController.setMainServerThread(mainServerThread); -// } catch (Exception e) { -// Alert alert = new Alert(AlertType.ERROR); -// alert.setHeaderText("Cannot host"); -// alert.setContentText("Oops, failed to host, try to restart."); -// alert.showAndWait(); -// e.printStackTrace(); -// } } /** - * ATTEMPTS TO: - * Connect to an ip address and port using the ip and port specified on start screen. - * Starts a Client To Server Thread to maintain connection to host. - * Switch view to lobby view. + * Creates an instance of GameClient and runs it has a client. */ @FXML public void connectButtonPressed() { @@ -127,28 +70,8 @@ public class StartScreenController implements Initializable { Sounds.playButtonClick(); gameClient = new GameClient(holder); gameClient.runAsClient(ipTextField.getText().trim().toLowerCase(), 4942); - -// try { -// String ipAddress = ipTextField.getText().trim().toLowerCase(); -// Integer port = Integer.valueOf(portTextField.getText().trim()); -// -//// ClientToServerThread clientToServerThread = new ClientToServerThread(ipAddress, port); -//// ClientState.setHost(false); -//// ClientState.setConnectedToHost(true); -// -//// controller.setClientToServerThread(clientToServerThread); -//// setContentPane("/views/LobbyView.fxml"); -// } catch (Exception e) { -// Alert alert = new Alert(AlertType.ERROR); -// alert.setHeaderText("Cannot reach the host"); -// alert.setContentText("Please check your host IP address."); -// alert.showAndWait(); -// } } -// public void setController(Controller controller) { -// this.controller = controller; -// } /** * Gets the local host ip address and sets this ip to ClientState. @@ -183,7 +106,6 @@ public class StartScreenController implements Initializable { if (ipAddress == null) { System.out.println("[HOST] Cannot obtain local host ip address."); } -// ClientState.setHostIp(ipAddress); return ipAddress; } diff --git a/src/main/java/seng302/visualiser/fxObjects/ChatHistory.java b/src/main/java/seng302/visualiser/fxObjects/ChatHistory.java new file mode 100644 index 00000000..c8f14c62 --- /dev/null +++ b/src/main/java/seng302/visualiser/fxObjects/ChatHistory.java @@ -0,0 +1,68 @@ +package seng302.visualiser.fxObjects; + +import java.util.Arrays; +import javafx.collections.ListChangeListener; +import javafx.scene.Node; +import javafx.scene.control.ScrollPane; +import javafx.scene.paint.Paint; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +/** + * Extension of a ScrollPane that contains a TextFlow. Has an addMessage() function to parse and + * display chatter text. + */ +public class ChatHistory extends ScrollPane { + + private TextFlow textFlow = new TextFlow(); + + public ChatHistory() { + this.setContent(textFlow); + this.setFitToWidth(true); + this.setFitToHeight(true); + this.setMaxHeight(Double.MAX_VALUE); + this.setMaxWidth(Double.MAX_VALUE); + this.setVbarPolicy(ScrollBarPolicy.ALWAYS); + this.setHbarPolicy(ScrollBarPolicy.NEVER); + //This makes the window auto scroll. + textFlow.getChildren().addListener((ListChangeListener) c -> + this.setVvalue(1.0) + ); + //This just makes it so that the ChatHistory is on focus it passes it off to the parent. + this.parentProperty().addListener((obs, old, parent) -> + this.focusedProperty().addListener((obsVal, oldVal, onFocus) -> { + if (onFocus) { + parent.requestFocus(); + } + }) + ); + } + + /** + * Adds a message to chat history. Messages should be either of the form: + * "[HH:MM:ss] \: \" or + * "SERVER: \" + * @param colour The colour of the user sending the message + * @param Text The chatter text message to be displayed + */ + public void addMessage (Paint colour, String Text) { + String[] words = Text.split(":"); + if (words[0].trim().equals("SERVER")) { + Text text = new Text(Text + "\n\n"); + text.setStyle("-fx-font-weight: bolder"); + textFlow.getChildren().add(text); + } else { + Text timePlayer = new Text( + String.join(":", Arrays.copyOfRange(words, 0, 3)) + ":" + ); + timePlayer.setStyle("-fx-font-weight: bold"); + timePlayer.setFill(colour); + Text message = new Text( + String.join(":", Arrays.copyOfRange(words, 3, words.length)) + "\n\n" + ); + message.wrappingWidthProperty().bind(this.widthProperty()); + textFlow.getChildren().addAll(timePlayer, message); + } + + } +} diff --git a/src/main/resources/views/RaceView.fxml b/src/main/resources/views/RaceView.fxml index d00f0099..f1473f5e 100644 --- a/src/main/resources/views/RaceView.fxml +++ b/src/main/resources/views/RaceView.fxml @@ -6,50 +6,41 @@ - - - - - - - - - - - - - - - - - - + - + - - + + - - + + - - + + + + + + + + + + +