From 8e128677e02affb5d7010d7162c07b668efafd86 Mon Sep 17 00:00:00 2001 From: J-Doiron <139803019+J-Doiron@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:55:42 -0800 Subject: [PATCH] Added Forza Motorsport Added a forza motorsport harness & some minor text changes on horizon --- forzamotorsport/README.md | 21 ++++ forzamotorsport/forzams.py | 174 +++++++++++++++++++++++++++++++ forzamotorsport/forzams_utils.py | 35 +++++++ forzamotorsport/manifest.yaml | 9 ++ horizonzdr/hzdr.py | 2 +- horizonzdr/hzdr_utils.py | 2 +- 6 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 forzamotorsport/README.md create mode 100644 forzamotorsport/forzams.py create mode 100644 forzamotorsport/forzams_utils.py create mode 100644 forzamotorsport/manifest.yaml diff --git a/forzamotorsport/README.md b/forzamotorsport/README.md new file mode 100644 index 0000000..5b640d1 --- /dev/null +++ b/forzamotorsport/README.md @@ -0,0 +1,21 @@ +# Forza Horizon 5 + +This script runs RTSS with the provided profile config and navigates through in-game menus to the built in benchmark and runs it with the current settings. + +## Prerequisites + +- Python 3.10+ +- Forza Horizon 5 installed via Steam +- Keras OCR service +- RTSS installed - https://rivatuner.net/ + +## 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 + +## Common Issues +1. "Login to Microsoft" modal pops up + - This game will not let you pass into the menu if you are not signed into Xbox. If you run this game at least once before running you can login then, or pre-login before running the harness. diff --git a/forzamotorsport/forzams.py b/forzamotorsport/forzams.py new file mode 100644 index 0000000..bb3424c --- /dev/null +++ b/forzamotorsport/forzams.py @@ -0,0 +1,174 @@ +"""Forza Motorsport test script""" +import os +import logging +import sys +import time +import pydirectinput as user +import winreg + +from hzdr_utils import get_resolution, get_args, process_registry_file + +sys.path.insert(1, os.path.join(sys.path[0], '..')) + +from harness_utils.keras_service import KerasService +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.misc import press_n_times +from harness_utils.process import terminate_processes +from harness_utils.steam import ( + exec_steam_run_command, + get_build_id +) +from harness_utils.artifacts import ArtifactManager, ArtifactType + +STEAM_GAME_ID = 2440510 +SCRIPT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) +LOG_DIRECTORY = os.path.join(SCRIPT_DIRECTORY, "run") +PROCESS_NAME = "forza_steamworks_release_final" +LOCAL_USER_SETTINGS = os.path.join( + os.getenv('LOCALAPPDATA'), "Microsoft.ForzaMotorsport", "User_SteamLocalStorageDirectory", + "ConnectedStorage", "ForzaUserConfigSelections", "UserConfigSelections" + ) + +user.FAILSAFE = False + + +def run_benchmark() -> tuple[float]: + """Run the benchmark""" + logging.info("Starting game") + exec_steam_run_command(STEAM_GAME_ID) + setup_start_time = time.time() + am = ArtifactManager(LOG_DIRECTORY) + + time.sleep(20) + + # Make sure the game started correctly + result = kerasService.look_for_word("settings", 10, 5) + if not result: + logging.info("Could not find the main menu. Did the game load?") + sys.exit(1) + + # Navigate to display menu + user.press("f") + time.sleep(1) + + if kerasService.wait_for_word(word="contrast", timeout=30, interval=1) is None: + logging.info("Did not find the video settings menu. Did the menu get stuck?") + sys.exit(1) + + user.press("]") + time.sleep(0.5) + user.press("]") + time.sleep(0.5) + user.press("]") + time.sleep(0.5) + + # Verify that we have navigated to the video settings menu and take a screenshot + if kerasService.wait_for_word(word="resolution", timeout=30, interval=1) is None: + logging.info("Did not find the video settings menu. Did the menu get stuck?") + sys.exit(1) + + am.take_screenshot("display.png", ArtifactType.CONFIG_IMAGE, "picture of display settings") + user.press("]") + time.sleep(0.5) + + if kerasService.wait_for_word(word="filtering", timeout=30, interval=1) is None: + logging.info("Did not find the video settings menu. Did the menu get stuck?") + sys.exit(1) + am.take_screenshot("graphics1.png", ArtifactType.CONFIG_IMAGE, "1st picture of graphics settings") + + press_n_times("down",15,0.5) + + if kerasService.wait_for_word(word="particle", timeout=30, interval=1) is None: + logging.info("Did not find the video settings menu. Did the menu get stuck?") + sys.exit(1) + am.take_screenshot("graphics2.png", ArtifactType.CONFIG_IMAGE, "2nd picture of graphics settings") + + press_n_times("down",3,0.5) + user.press("up") + time.sleep(0.5) + user.press("down") + time.sleep(0.5) + + if kerasService.wait_for_word(word="flare", timeout=30, interval=1) is None: + logging.info("Did not find the video settings menu. Did the menu get stuck?") + sys.exit(1) + am.take_screenshot("graphics3.png", ArtifactType.CONFIG_IMAGE, "3rd picture of graphics settings") + + # Navigate to graphics menu + user.press("[") + time.sleep(0.5) + user.press("enter") + + setup_end_time = time.time() + elapsed_setup_time = round((setup_end_time - setup_start_time), 2) + logging.info("Setup took %s seconds", elapsed_setup_time) + + time.sleep(15) + + if kerasService.wait_for_word(word="results", timeout=30, interval=1) is None: + logging.info("Did not find the video settings menu. Did the menu get stuck?") + sys.exit(1) + am.take_screenshot("results.png", ArtifactType.CONFIG_IMAGE, "picture of results screen") + + test_start_time = time.time() + + # Wait for benchmark to complete + time.sleep(180) + + # Wait for results screen to display info + result = kerasService.wait_for_word("results", interval=0.1, timeout=11) + if not result: + logging.info( + "Didn't see signal lost. Could not mark the proper end time!") + + test_end_time = round(time.time()) + # Give results screen time to fill out, then save screenshot and config file + time.sleep(2) + am.copy_file(LOCAL_USER_SETTINGS, ArtifactType.CONFIG_TEXT, "config file") + + elapsed_test_time = round((test_end_time - test_start_time), 2) + logging.info("Benchmark took %s 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) + +args = get_args() +kerasService = KerasService(args.keras_host, args.keras_port) + +try: + start_time, end_time = run_benchmark() + height, width = get_resolution(LOCAL_USER_SETTINGS) + 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/forzamotorsport/forzams_utils.py b/forzamotorsport/forzams_utils.py new file mode 100644 index 0000000..2b6a8b7 --- /dev/null +++ b/forzamotorsport/forzams_utils.py @@ -0,0 +1,35 @@ +"""Utility functions supporting Forza Motorsport test script.""" +from argparse import ArgumentParser +import re + + +def get_resolution(config_file: str) -> tuple[int]: + """Retrieve the resolution from local configuration files.""" + width_pattern = re.compile(r"\"FullscreenWidth\"=(\d+)") + height_pattern = re.compile(r"\"FullscreenHeight\"=(\d+)") + width = 0 + height = 0 + + with open(config_file, encoding="utf-8") as file: + lines = file.readlines() + for line in lines: + width_match = width_pattern.match(line) + height_match = height_pattern.match(line) + + if width_match: + width = width_match.group(1) + if height_match: + height = height_match.group(1) + + return (height, width) + +def get_args() -> any: + """Get command line arguments""" + 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) + return parser.parse_args() + + diff --git a/forzamotorsport/manifest.yaml b/forzamotorsport/manifest.yaml new file mode 100644 index 0000000..071889a --- /dev/null +++ b/forzamotorsport/manifest.yaml @@ -0,0 +1,9 @@ +friendly_name: "Forza Motorsport" +executable: "forzams.py" +process_name: "forza_steamworks_release_final.exe" +output_dir: "run" +options: + - name: kerasHost + type: input + - name: kerasPort + type: input diff --git a/horizonzdr/hzdr.py b/horizonzdr/hzdr.py index 4cf5315..6ebca7a 100644 --- a/horizonzdr/hzdr.py +++ b/horizonzdr/hzdr.py @@ -1,4 +1,4 @@ -"""Returnal test script""" +"""Horizon Zero Dawn Remastered test script""" import os import logging import sys diff --git a/horizonzdr/hzdr_utils.py b/horizonzdr/hzdr_utils.py index 6241041..f148b00 100644 --- a/horizonzdr/hzdr_utils.py +++ b/horizonzdr/hzdr_utils.py @@ -1,4 +1,4 @@ -"""Utility functions supporting Returnal test script.""" +"""Utility functions supporting Horizon Zero Dawn Remastered test script.""" from argparse import ArgumentParser import re import winreg