From 189fa88405eed9d533f903310293f758e44746d8 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Fri, 11 Oct 2019 22:42:31 +0200 Subject: [PATCH 1/6] Added RLBot runner process and socket communication. --- .../tournament/rlbot/MatchRunner.java | 84 +++++++++++++++++-- .../ds306e18/tournament/settings/files/run.py | 80 +++++++++++++++++- 2 files changed, 154 insertions(+), 10 deletions(-) diff --git a/src/dk/aau/cs/ds306e18/tournament/rlbot/MatchRunner.java b/src/dk/aau/cs/ds306e18/tournament/rlbot/MatchRunner.java index 50eccea..a0f74ca 100644 --- a/src/dk/aau/cs/ds306e18/tournament/rlbot/MatchRunner.java +++ b/src/dk/aau/cs/ds306e18/tournament/rlbot/MatchRunner.java @@ -10,15 +10,44 @@ import dk.aau.cs.ds306e18.tournament.utility.Alerts; import java.io.*; +import java.net.ConnectException; +import java.net.Socket; import java.nio.file.Path; import java.util.ArrayList; +/** + * This class maintains a RLBot runner process for starting matches. The runner process is the run.py running in + * a separate command prompt. Communication between CleoPetra and the RLBot runner happens through a socket and + * consists of simple commands like STOP, STOP, EXIT, which are defined in the subclass Command. If the RLBot runner + * is not running when a command is issued, a new instance of the RLBot runner is started. + */ public class MatchRunner { // The first %s will be replaced with the directory of the rlbot.cfg. The second %s will be the drive 'C:' to change drive. private static final String COMMAND_FORMAT = "cmd.exe /c start cmd /c \"cd %s & %s & python run.py\""; + private static final String ADDR = "127.0.0.1"; + private static final int PORT = 35353; // TODO Make user able to change the port in a settings file - /** Starts the given match in Rocket League. */ + private enum Command { + START("START"), // Start the match described by the rlbot.cfg + STOP("STOP"), // Stop the current match and all bot pids + EXIT("EXIT"); // Close the run.py process + + private final String cmd; + + Command(String cmd) { + this.cmd = cmd; + } + + @Override + public String toString() { + return cmd; + } + } + + /** + * Starts the given match in Rocket League. + */ public static boolean startMatch(MatchConfig matchConfig, Match match) { // Checks settings and modifies rlbot.cfg file if everything is okay @@ -27,21 +56,60 @@ public static boolean startMatch(MatchConfig matchConfig, Match match) { return false; } + return sendCommandToRLBot(Command.START, true); + } + + /** + * Start the RLBot runner in the run.py and will be a separate process in a separate cmd. + * The runner might not be ready to accept commands immediately. This method returns true on success. + */ + private static boolean startRLBotRunner() { try { + Alerts.infoNotification("Starting RLBot runner", "Attempting to start new instance of run.py for running matches."); Path pathToDirectory = SettingsDirectory.RUN_PY.getParent(); - String command = String.format(COMMAND_FORMAT, pathToDirectory, pathToDirectory.toString().substring(0, 2)); - System.out.println("Starting RLBot framework with command: " + command); - Runtime.getRuntime().exec(command); + String cmd = String.format(COMMAND_FORMAT, pathToDirectory, pathToDirectory.toString().substring(0, 2)); + Runtime.getRuntime().exec(cmd); return true; - - } catch (Exception err) { - err.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + Alerts.errorNotification("Could not start RLBot runner", "Something went wrong starting the run.py."); + return false; } + } + /** + * Sends the given command to the RLBot runner. If the runner does not respond, this method will optionally + * start a new RLBot runner instance and retry sending the command. If we believe the command was send + * and received, this method returns true, otherwise false. + */ + private static boolean sendCommandToRLBot(Command cmd, boolean startRLBotIfMissingAndRetry) { + try (Socket sock = new Socket(ADDR, PORT); + PrintWriter writer = new PrintWriter(sock.getOutputStream(), true)) { + writer.print(cmd.toString()); + writer.flush(); + return true; + } catch (ConnectException e) { + // The run.py did not respond. Starting a new instance if allowed + if (startRLBotIfMissingAndRetry) { + try { + startRLBotRunner(); + Thread.sleep(200); + // Retry + return sendCommandToRLBot(cmd, false); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + } catch (IOException e) { + e.printStackTrace(); + Alerts.errorNotification("IO Exception", "Failed to open socket and send message to run.py"); + } return false; } - /** Returns true if the given match can be started. */ + /** + * Returns true if the given match can be started. + */ public static boolean canStartMatch(Match match) { try { checkMatch(match); diff --git a/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py b/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py index ba16922..15fcd27 100644 --- a/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py +++ b/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py @@ -1,10 +1,86 @@ +import time +import socket +from threading import Event, Thread + +from rlbot.setup_manager import SetupManager, setup_manager_context + + +PORT = 35353 + + +def setup_match(manager: SetupManager): + manager.shut_down(kill_all_pids=True, quiet=True) # Stop any running pids + manager.load_config() + manager.launch_early_start_bot_processes() + manager.start_match() + manager.launch_bot_processes() + + +def wait_for_all_bots(manager: SetupManager): + while not manager.has_received_metadata_from_all_bots(): + manager.try_recieve_agent_metadata() + time.sleep(0.1) + + +def start_match(manager: SetupManager): + game_interface = manager.game_interface + setup_match(manager) + wait_for_all_bots(manager) + + +def stop_match(manager: SetupManager): + manager.shut_down(kill_all_pids=True, quiet=True) + + +def match_running(start_event: Event, stop_event: Event): + with setup_manager_context() as manager: + while True: + if start_event.is_set(): + start_match(manager) + start_event.clear() + elif stop_event.is_set(): + stop_match(manager) + stop_event.clear() + + if __name__ == '__main__': + print("""/<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>\\ +| Hello! I am CleoPetra's little helper | +| and your view into the RLBot process. | +| Keep me open while using CleoPetra! | +\\<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>/ +""") try: - from rlbot import runner - runner.main() + start_event = Event() + stop_event = Event() + + #Thread(args=(start_event, stop_event)).start() + + # AF_INET is the Internet address family for IPv4. SOCK_STREAM is the socket type for TCP + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', PORT)) + s.listen() + print(f"Listening for CleoPetra on 127.0.0.1:{PORT} ...") + while True: + conn, addr = s.accept() + with conn: + print('Connected by', addr) + data = conn.recv(1024) + if data == b"START": + print("Starting match!") + elif data == b"STOP": + print("Stopping match!") + elif data == b"QUIT": + print("Quiting!") + break + else: + print(f"Other message: {data}") + conn.sendall(b'Gotcha from Python!') except Exception as e: print("Encountered exception: ", e) print("Press enter to close.") input() + finally: + print("run.py stopped") From 3eee2d7a680322ec762d569698b9edc00b47c8c1 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sat, 12 Oct 2019 10:51:57 +0200 Subject: [PATCH 2/6] Commands now starts and stops matches. --- .../ds306e18/tournament/settings/files/run.py | 34 ++++++++++++++----- .../ds306e18/tournament/utility/Alerts.java | 4 +-- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py b/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py index 15fcd27..e73acfb 100644 --- a/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py +++ b/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py @@ -8,6 +8,12 @@ PORT = 35353 +class Command: + START = b"START" + STOP = b"STOP" + EXIT = b"EXIT" + + def setup_match(manager: SetupManager): manager.shut_down(kill_all_pids=True, quiet=True) # Stop any running pids manager.load_config() @@ -32,7 +38,7 @@ def stop_match(manager: SetupManager): manager.shut_down(kill_all_pids=True, quiet=True) -def match_running(start_event: Event, stop_event: Event): +def match_running(start_event: Event, stop_event: Event, exit_event: Event): with setup_manager_context() as manager: while True: if start_event.is_set(): @@ -41,6 +47,9 @@ def match_running(start_event: Event, stop_event: Event): elif stop_event.is_set(): stop_match(manager) stop_event.clear() + elif exit_event.is_set(): + exit_event.clear() + break if __name__ == '__main__': @@ -54,8 +63,10 @@ def match_running(start_event: Event, stop_event: Event): try: start_event = Event() stop_event = Event() + exit_event = Event() - #Thread(args=(start_event, stop_event)).start() + match_runner = Thread(target=match_running, args=(start_event, stop_event, exit_event)) + match_runner.start() # AF_INET is the Internet address family for IPv4. SOCK_STREAM is the socket type for TCP with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: @@ -67,16 +78,23 @@ def match_running(start_event: Event, stop_event: Event): with conn: print('Connected by', addr) data = conn.recv(1024) - if data == b"START": + + if data == Command.START: print("Starting match!") - elif data == b"STOP": + start_event.set() + elif data == Command.STOP: print("Stopping match!") - elif data == b"QUIT": - print("Quiting!") + stop_event.set() + elif data == Command.EXIT: + print("Exiting!") + exit_event.set() break else: - print(f"Other message: {data}") - conn.sendall(b'Gotcha from Python!') + print(f"Unknown command received: {data}") + + conn.sendall(b'Gotcha!') + + match_runner.join() except Exception as e: print("Encountered exception: ", e) diff --git a/src/dk/aau/cs/ds306e18/tournament/utility/Alerts.java b/src/dk/aau/cs/ds306e18/tournament/utility/Alerts.java index 88b9f57..7dbaf46 100644 --- a/src/dk/aau/cs/ds306e18/tournament/utility/Alerts.java +++ b/src/dk/aau/cs/ds306e18/tournament/utility/Alerts.java @@ -22,7 +22,7 @@ public static void infoNotification (String title, String text){ .title(title) .text(text) .graphic(null) - .hideAfter(Duration.seconds(3)) + .hideAfter(Duration.seconds(5)) .position(Pos.BOTTOM_RIGHT) .owner(window) .showInformation(); @@ -38,7 +38,7 @@ public static void errorNotification (String title, String text){ .title(title) .text(text) .graphic(null) - .hideAfter(Duration.seconds(5)) + .hideAfter(Duration.seconds(8)) .position(Pos.BOTTOM_RIGHT) .owner(window) .showError(); From c5404797b6d8ff69af32d36af162a2e576f006a5 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sat, 12 Oct 2019 11:25:32 +0200 Subject: [PATCH 3/6] Added Open, Close, and Stop bottom in the RLBotSettings tab. --- .../tournament/rlbot/MatchRunner.java | 19 ++++++++++++-- .../ds306e18/tournament/settings/files/run.py | 2 +- .../ui/RLBotSettingsTabController.java | 16 ++++++++++++ .../ui/layout/RLBotSettingsTab.fxml | 26 +++++++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/dk/aau/cs/ds306e18/tournament/rlbot/MatchRunner.java b/src/dk/aau/cs/ds306e18/tournament/rlbot/MatchRunner.java index dd4f4c1..bc86d65 100644 --- a/src/dk/aau/cs/ds306e18/tournament/rlbot/MatchRunner.java +++ b/src/dk/aau/cs/ds306e18/tournament/rlbot/MatchRunner.java @@ -74,10 +74,10 @@ public static boolean startMatch(MatchConfig matchConfig, Match match) { } /** - * Start the RLBot runner in the run.py and will be a separate process in a separate cmd. + * Starts the RLBot runner, aka. the run.py, as a separate process in a separate cmd. * The runner might not be ready to accept commands immediately. This method returns true on success. */ - private static boolean startRLBotRunner() { + public static boolean startRLBotRunner() { try { Alerts.infoNotification("Starting RLBot runner", "Attempting to start new instance of run.py for running matches."); Path pathToDirectory = SettingsDirectory.RUN_PY.getParent(); @@ -91,6 +91,21 @@ private static boolean startRLBotRunner() { } } + /** + * Closes the RLBot runner. + */ + public static void closeRLBotRunner() { + // If the command fails, the runner is probably not running anyway, so ignore any errors. + sendCommandToRLBot(Command.EXIT, false); + } + + /** + * Stops the current match. + */ + public static void stopMatch() { + sendCommandToRLBot(Command.STOP, false); + } + /** * Sends the given command to the RLBot runner. If the runner does not respond, this method will optionally * start a new RLBot runner instance and retry sending the command. If we believe the command was send diff --git a/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py b/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py index e73acfb..34863a9 100644 --- a/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py +++ b/src/dk/aau/cs/ds306e18/tournament/settings/files/run.py @@ -101,4 +101,4 @@ def match_running(start_event: Event, stop_event: Event, exit_event: Event): print("Press enter to close.") input() finally: - print("run.py stopped") + print("run.py stopped. You can close this!") diff --git a/src/dk/aau/cs/ds306e18/tournament/ui/RLBotSettingsTabController.java b/src/dk/aau/cs/ds306e18/tournament/ui/RLBotSettingsTabController.java index 25d32dd..367dd89 100644 --- a/src/dk/aau/cs/ds306e18/tournament/ui/RLBotSettingsTabController.java +++ b/src/dk/aau/cs/ds306e18/tournament/ui/RLBotSettingsTabController.java @@ -1,6 +1,7 @@ package dk.aau.cs.ds306e18.tournament.ui; import dk.aau.cs.ds306e18.tournament.model.Tournament; +import dk.aau.cs.ds306e18.tournament.rlbot.MatchRunner; import dk.aau.cs.ds306e18.tournament.rlbot.RLBotSettings; import dk.aau.cs.ds306e18.tournament.rlbot.configuration.MatchConfig; import dk.aau.cs.ds306e18.tournament.rlbot.configuration.MatchConfigOptions; @@ -26,6 +27,9 @@ public class RLBotSettingsTabController { public RadioButton skipReplaysRadioButton; public RadioButton instantStartRadioButton; public RadioButton writeOverlayDataRadioButton; + public Button rlbotRunnerOpenButton; + public Button rlbotRunnerCloseButton; + public Button rlbotRunnerStopMatchButton; public ChoiceBox matchLengthChoiceBox; public ChoiceBox maxScoreChoiceBox; public ChoiceBox overtimeChoiceBox; @@ -137,4 +141,16 @@ public void update() { // Other settings writeOverlayDataRadioButton.setSelected(settings.writeOverlayData()); } + + public void onActionRLBotRunnerOpen(ActionEvent actionEvent) { + MatchRunner.startRLBotRunner(); + } + + public void onActionRLBotRunnerClose(ActionEvent actionEvent) { + MatchRunner.closeRLBotRunner(); + } + + public void onActionRLBotRunnerStopMatch(ActionEvent actionEvent) { + MatchRunner.stopMatch(); + } } diff --git a/src/dk/aau/cs/ds306e18/tournament/ui/layout/RLBotSettingsTab.fxml b/src/dk/aau/cs/ds306e18/tournament/ui/layout/RLBotSettingsTab.fxml index 60b6528..ef3434f 100644 --- a/src/dk/aau/cs/ds306e18/tournament/ui/layout/RLBotSettingsTab.fxml +++ b/src/dk/aau/cs/ds306e18/tournament/ui/layout/RLBotSettingsTab.fxml @@ -126,6 +126,32 @@ + + + + + + + + + + + + + + + + + + + + + +