diff --git a/doomdarkages/README.md b/doomdarkages/README.md new file mode 100644 index 0000000..fd84e33 --- /dev/null +++ b/doomdarkages/README.md @@ -0,0 +1,21 @@ +# F1 24 + +This script navigates through in-game menus to the built in benchmark and runs it with the current settings. It then waits for a results screen, expecting the benchmark to be running 3 laps. + +## Prerequisites + +- Python 3.10+ +- F1 24 installed +- Keras OCR service + +## Options + +- `kerasHost`: string representing the IP address of the Keras service. e.x. `0.0.0.0` +- `kerasPort`: string representing the port of the Keras service. e.x. `8080` + +## Output + +report.json +- `resolution`: string representing the resolution the test was run at, formatted as "[width]x[height]", e.x. `1920x1080` +- `start_time`: number representing a timestamp of the test's start time in milliseconds +- `end_time`: number representing a timestamp of the test's end time in milliseconds \ No newline at end of file diff --git a/doomdarkages/doomdarkages.py b/doomdarkages/doomdarkages.py new file mode 100644 index 0000000..d0695a6 --- /dev/null +++ b/doomdarkages/doomdarkages.py @@ -0,0 +1,271 @@ +"""F1 24 test script""" +import logging +from argparse import ArgumentParser +import os.path +import re +import time +import sys +import pydirectinput as user +from doomdarkages_utils import get_resolution + +sys.path.insert(1, os.path.join(sys.path[0], "..")) + +from doomdarkages_utils import copy_launcher_config +from harness_utils.steam import exec_steam_game, get_app_install_location, get_build_id +from harness_utils.keras_service import KerasService +from harness_utils.misc import remove_files, press_n_times +from harness_utils.process import terminate_processes +from harness_utils.output import ( + format_resolution, + seconds_to_milliseconds, + setup_log_directory, + write_report_json, + DEFAULT_LOGGING_FORMAT, + DEFAULT_DATE_FORMAT, +) +from harness_utils.artifacts import ArtifactManager, ArtifactType + +SCRIPT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) +LOG_DIRECTORY = os.path.join(SCRIPT_DIRECTORY, "run") +PROCESS_NAME = "F1_24" +STEAM_GAME_ID = 3017860 +VIDEO_PATH = os.path.join(get_app_install_location(STEAM_GAME_ID), "videos") +VIDEO_PATH = os.path.join(get_app_install_location(STEAM_GAME_ID), "videos") + +username = os.getlogin() +CONFIG_PATH = f"C:\\Users\\{username}\\Saved Games\\id Software\\DOOMTheDarkAges\\base" +CONFIG_FILENAME = "hardware_settings_config.xml" +CONFIG = f"{CONFIG_PATH}\\{CONFIG_FILENAME}" +#BENCHMARK_RESULTS_PATH = f"C:\\Users\\{username}\\Documents\\My Games\\F1 24\\benchmark" + +intro_videos = [ + os.path.join(VIDEO_PATH, "attract.bk2"), + os.path.join(VIDEO_PATH, "cm_f1_sting.bk2") +] + +def start_game(): + """Launch the game with no launcher or start screen""" + copy_launcher_config() + return exec_steam_game(STEAM_GAME_ID, game_params=["+com_skipIntroVideo 1"]) + +def find_latest_result_file(base_path): + """Look for files in the benchmark results path that match the pattern in the regular expression""" + pattern = r"benchmark_.*\.xml" + list_of_files = [] + + for filename in os.listdir(base_path): + if re.search(pattern, filename, re.IGNORECASE): + list_of_files.append(base_path + '\\' +filename) + + latest_file = max(list_of_files, key=os.path.getmtime) + + return latest_file + +def find_settings() -> any: + """Look for and enter settings""" + if not kerasService.look_for_word("settings", attempts=5, interval=3): + logging.info("Didn't find settings!") + sys.exit(1) + user.press("enter") + time.sleep(1.5) + + +def find_graphics() -> any: + """Look for and enter graphics settings""" + if not kerasService.look_for_word("graphics", attempts=5, interval=3): + logging.info("Didn't find graphics!") + sys.exit(1) + user.press("right") + time.sleep(0.2) + user.press("enter") + time.sleep(1.5) + + +def navigate_startup(): + """press space through the warnings and navigate startup menus""" + result = kerasService.wait_for_word("product", timeout=80) + if not result: + logging.info("Game didn't start in time. Check settings and try again.") + sys.exit(1) + + user.press("space") + time.sleep(1) + user.press("space") + time.sleep(1) + user.press("space") + time.sleep(4) + + # Press enter to proceed to the main menu + result = kerasService.wait_for_word("press", interval=2, timeout=80) + if not result: + logging.info("Game didn't start in time. Check settings and try again.") + sys.exit(1) + + logging.info("Hit the title screen. Continuing") + user.press("enter") + time.sleep(1) + + # cancel logging into ea services + result = kerasService.wait_for_word("login", timeout=50) + if result: + logging.info("Cancelling logging in.") + user.press("enter") + time.sleep(2) + +def run_benchmark(): + """Runs the actual benchmark.""" + remove_files(intro_videos) + start_game() + am = ArtifactManager(LOG_DIRECTORY) + + setup_start_time = int(time.time()) + time.sleep(2) + navigate_startup() + + # Navigate menus and take screenshots using the artifact manager + result = kerasService.wait_for_word("theatre", interval=3, timeout=60) + if not result: + logging.info("Didn't land on the main menu!") + sys.exit(1) + + logging.info("Saw the options! we are good to go!") + time.sleep(1) + + press_n_times("down", 6, 0.2) + user.press("enter") + time.sleep(2) + #find_settings() + find_graphics() + + # Navigate to video settings + press_n_times("down", 3, 0.2) + user.press("enter") + time.sleep(0.2) + + result = kerasService.wait_for_word("vsync", interval=3, timeout=60) + if not result: + logging.info("Didn't find the keyword 'vsync'. Did the program navigate to the video mode menu correctly?") + sys.exit(1) + press_n_times("down", 18, 0.2) + + am.take_screenshot("video.png", ArtifactType.CONFIG_IMAGE, "screenshot of video settings menu") + user.press("esc") + time.sleep(0.2) + + result = kerasService.wait_for_word("steering", interval=3, timeout=60) + if not result: + logging.info("Didn't find the keyword 'steering'. Did the program exit the video mode menu correctly?") + sys.exit(1) + + # Navigate through graphics settings and take screenshots of all settings contained within + am.take_screenshot("graphics_1.png", ArtifactType.CONFIG_IMAGE, "first screenshot of graphics settings") + press_n_times("down", 29, 0.2) + + result = kerasService.wait_for_word("chromatic", interval=3, timeout=60) + if not result: + logging.info("Didn't find the keyword 'chromatic'. Did we navigate the menu correctly?") + sys.exit(1) + + am.take_screenshot("graphics_2.png", ArtifactType.CONFIG_IMAGE, "second screenshot of graphics settings") + press_n_times("up", 28, 0.2) + user.press("enter") + time.sleep(0.2) + + # Navigate benchmark menu + if not kerasService.look_for_word("weather", attempts=5, interval=3): + logging.info("Didn't find weather!") + sys.exit(1) + + am.take_screenshot("benchmark.png", ArtifactType.CONFIG_IMAGE, "screenshot of benchmark settings") + + press_n_times("down", 6, 0.2) + user.press("enter") + time.sleep(2) + + elapsed_setup_time = round(int(time.time()) - setup_start_time, 2) + logging.info("Setup took %f seconds", elapsed_setup_time) + + result = kerasService.wait_for_word("lap", interval=0.5, timeout=90) + if not result: + logging.info("Benchmark didn't start.") + sys.exit(1) + + logging.info("Benchmark started. Waiting for benchmark to complete.") + test_start_time = int(time.time()) + 8 + + # sleep for 3 laps + time.sleep(310) + + test_end_time = None + + result = kerasService.wait_for_word("loading", interval=0.5, timeout=90) + if result: + logging.info("Found the loading screen. Marking the out time.") + test_end_time = int(time.time()) - 2 + time.sleep(2) + else: + logging.info("Could not find the loading screen. Could not mark end time!") + + result = kerasService.wait_for_word("results", interval=3, timeout=90) + if not result: + logging.info("Results screen was not found!" + + "Did harness not wait long enough? Or test was too long?") + sys.exit(1) + logging.info("Results screen was found! Finishing benchmark.") + results_file = find_latest_result_file(BENCHMARK_RESULTS_PATH) + am.take_screenshot("result.png", ArtifactType.RESULTS_IMAGE, "screenshot of results") + am.copy_file(CONFIG, ArtifactType.CONFIG_TEXT, "config file") + am.copy_file(results_file, ArtifactType.RESULTS_TEXT, "benchmark results xml file") + + if test_end_time is None: + logging.info("Loading screen end time not found. Using results screen fallback time.") + test_end_time = int(time.time()) + + elapsed_test_time = round(test_end_time - test_start_time, 2) + logging.info("Benchmark took %f seconds", elapsed_test_time) + + terminate_processes(PROCESS_NAME) + am.create_manifest() + + return test_start_time, test_end_time + + +setup_log_directory(LOG_DIRECTORY) + +logging.basicConfig( + filename=f"{LOG_DIRECTORY}/harness.log", + format=DEFAULT_LOGGING_FORMAT, + datefmt=DEFAULT_DATE_FORMAT, + level=logging.DEBUG, +) +console = logging.StreamHandler() +formatter = logging.Formatter(DEFAULT_LOGGING_FORMAT) +console.setFormatter(formatter) +logging.getLogger("").addHandler(console) + +parser = ArgumentParser() +parser.add_argument( + "--kerasHost", dest="keras_host", help="Host for Keras OCR service", required=True +) +parser.add_argument( + "--kerasPort", dest="keras_port", help="Port for Keras OCR service", required=True +) +args = parser.parse_args() +kerasService = KerasService(args.keras_host, args.keras_port) + +try: + start_time, end_time = run_benchmark() + width, height = get_resolution() + report = { + "resolution": format_resolution(width, height), + "start_time": seconds_to_milliseconds(start_time), + "end_time": seconds_to_milliseconds(end_time), + "version": get_build_id(STEAM_GAME_ID) + } + + write_report_json(LOG_DIRECTORY, "report.json", report) +except Exception as e: + logging.error("Something went wrong running the benchmark!") + logging.exception(e) + terminate_processes(PROCESS_NAME) + sys.exit(1) diff --git a/doomdarkages/doomdarkages_utils.py b/doomdarkages/doomdarkages_utils.py new file mode 100644 index 0000000..f89c541 --- /dev/null +++ b/doomdarkages/doomdarkages_utils.py @@ -0,0 +1,46 @@ +"""Utility functions supporting F1 24 test script.""" +import os +import re +from pathlib import Path +import logging +import shutil + +from harness_utils.steam import get_app_install_location +SCRIPT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) +STEAM_GAME_ID = 3017860 + + +def get_resolution() -> tuple[int]: + """Gets resolution width and height from local xml file created by game.""" + username = os.getlogin() + config_path = f"C:\\Users\\{username}\\Documents\\My Games\\F1 24\\hardwaresettings" + config_filename = "hardware_settings_config.xml" + resolution = re.compile(r" None: + """Copy benchmark config to dota 2 folder""" + try: + LAUNCHERCONFIG_PATH = os.path.join(get_app_install_location(STEAM_GAME_ID), "launcherData", "base", "configs") + LAUNCHERCONFIG_PATH.mkdir(parents=True, exist_ok=True) + + src_path = SCRIPT_DIRECTORY / "launcher.cfg" + dest_path = LAUNCHERCONFIG_PATH / "launcher.cfg" + + logging.info("Copying: %s -> %s", src_path, dest_path) + shutil.copy(src_path, dest_path) + except OSError as err: + logging.error("Could not copy config file.") + raise err \ No newline at end of file diff --git a/doomdarkages/manifest.yaml b/doomdarkages/manifest.yaml new file mode 100644 index 0000000..28d7abb --- /dev/null +++ b/doomdarkages/manifest.yaml @@ -0,0 +1,10 @@ +friendly_name: "Doom: The Dark Ages" +executable: "doomdarkages.py" +process_name: "F1_24.exe" +hidden: 0 +output_dir: "run" +options: + - name: kerasHost + type: input + - name: kerasPort + type: input \ No newline at end of file