Audit Forza Horizon 5 (#42)

* audit forza scripts

* remove preset from report json

* update readme

* fix key error with keras results

* remove commented out code

* add rtss info to readme
This commit is contained in:
derek-hirotsu
2023-09-22 10:32:27 -07:00
committed by GitHub
parent fbb0100e3b
commit 3135d6ee96
4 changed files with 99 additions and 113 deletions

2
.gitignore vendored
View File

@@ -13,6 +13,8 @@ flac-1.4.3-win/
flac-1.4.3-win.zip flac-1.4.3-win.zip
y-cruncher v0.8.2.9522/ y-cruncher v0.8.2.9522/
y-cruncher v0.8.2.9522.zip y-cruncher v0.8.2.9522.zip
csgo-benchmark-master/
csgo-benchmark-master.zip
# python # python
__pycache__/ __pycache__/

View File

@@ -1,11 +1,20 @@
# Forza Horizon 5 # Forza Horizon 5
TODO This script runs RTSS with the proivded profile config and navigates through in-game menus to the built in benchmark and runs it with the current settings.
## Prerequisites ## Prerequisites
- Python 3.10+ - Python 3.10+
- Forza Horizon 5 installed via Steam. - 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[heigt]", 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 ## Common Issues
1. "Login to Microsoft" modal pops up 1. "Login to Microsoft" modal pops up

View File

@@ -1,148 +1,121 @@
import logging """Forza Horizon 5 test script"""
import sys
from argparse import ArgumentParser from argparse import ArgumentParser
import logging
import os import os
import pydirectinput as user import time
import sys
import pyautogui as gui import pyautogui as gui
import time import pydirectinput as user
from forza5_utils import read_resolution from forza5_utils import read_resolution
sys.path.insert(1, os.path.join(sys.path[0], '..')) sys.path.insert(1, os.path.join(sys.path[0], '..'))
from harness_utils.output import * # pylint: disable=wrong-import-position
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.process import terminate_processes from harness_utils.process import terminate_processes
from harness_utils.rtss import start_rtss_process, copy_rtss_profile from harness_utils.rtss import start_rtss_process, copy_rtss_profile
from harness_utils.steam import exec_steam_run_command from harness_utils.steam import exec_steam_run_command
from harness_utils.keras_service import KerasService from harness_utils.keras_service import KerasService
# pylint: enable=wrong-import-position
STEAM_GAME_ID = 1551360 STEAM_GAME_ID = 1551360
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
LOG_DIRECTORY = os.path.join(SCRIPT_DIR, "run") LOG_DIRECTORY = os.path.join(SCRIPT_DIR, "run")
APPDATALOCAL = os.getenv("LOCALAPPDATA") APPDATALOCAL = os.getenv("LOCALAPPDATA")
CONFIG_LOCATION = f"{APPDATALOCAL}\\ForzaHorizon5\\User_SteamLocalStorageDirectory\\ConnectedStorage\\ForzaUserConfigSelections" CONFIG_LOCATION = (
f"{APPDATALOCAL}\\ForzaHorizon5\\User_SteamLocalStorageDirectory"
"\\ConnectedStorage\\ForzaUserConfigSelections"
)
CONFIG_FILENAME = "UserConfigSelections" CONFIG_FILENAME = "UserConfigSelections"
PROCESSES = ["ForzaHorizon5.exe", "RTSS.exe"] PROCESSES = ["ForzaHorizon5.exe", "RTSS.exe"]
def start_rtss(): def start_rtss():
"""Sets up the RTSS process"""
profile_path = os.path.join(SCRIPT_DIR, "ForzaHorizon5.exe.cfg") profile_path = os.path.join(SCRIPT_DIR, "ForzaHorizon5.exe.cfg")
copy_rtss_profile(profile_path) copy_rtss_profile(profile_path)
return start_rtss_process() return start_rtss_process()
def is_word_on_screen(word: str, attempts: int = 5, delay_seconds: int = 1) -> bool:
for _ in range(attempts):
result = kerasService.capture_screenshot_find_word(word)
if result != None:
return True
time.sleep(delay_seconds)
return False
def is_word_clickable(word: str, attempts: int = 5, delay_seconds: int = 1) -> bool:
for _ in range(attempts):
result = kerasService.capture_screenshot_find_word(word)
if result != None:
return (result["x"], result["y"])
time.sleep(delay_seconds)
return None
def accessibility() -> any:
return is_word_on_screen(word="start", attempts=10, delay_seconds=1)
def graphics() -> any:
return is_word_clickable(word="graphics", attempts=10, delay_seconds=1)
def benchmark() -> any:
return is_word_clickable(word="benchmark", attempts=10, delay_seconds=1)
def results() -> any:
return is_word_on_screen(word="results", attempts=25, delay_seconds=1)
def checkpoint() -> any:
return is_word_on_screen(word="checkpoint", attempts=10, delay_seconds=1)
def run_benchmark(): def run_benchmark():
"""Starts the benchmark"""
start_rtss() start_rtss()
# Give RTSS time to start # Give RTSS time to start
time.sleep(10) time.sleep(10)
exec_steam_run_command(STEAM_GAME_ID) exec_steam_run_command(STEAM_GAME_ID)
t1 = time.time() setup_start_time = time.time()
# Wait for menu to load # Wait for menu to load
time.sleep(30) time.sleep(30)
menu_screen_wait = time.time()
while (True):
accessibility()
if accessibility():
start_time = time.time()
logging.info("Accessibilty found pressing X to continue.")
user.press("x")
time.sleep(2)
break
elif time.time()-menu_screen_wait > 60:
logging.info("Game didn't start.")
exit(1)
logging.info("Game hasn't started. Trying again in 10 seconds")
time.sleep(10)
graphics_wait = time.time() logging.info("Waiting for start prompt...")
while (True): result = kerasService.wait_for_word("start", timeout=30)
graphics() if not result:
if graphics(): logging.info("Game didn't start.")
logging.info("Graphics found, clicking and continuing.") sys.exit(1)
graphics_click = graphics()
gui.moveTo(graphics_click[0], graphics_click[1])
time.sleep(0.2)
gui.mouseDown()
time.sleep(0.2)
gui.mouseUp()
time.sleep(1)
break
elif time.time()-graphics_wait > 60:
logging.info("Game didn't load to the settings menu.")
exit(1)
logging.info("Menu hasn't loaded yet. Trying again in 10 seconds")
time.sleep(10)
if benchmark(): logging.info("Accessibilty found pressing X to continue.")
logging.info("Benchmark found, clicking and starting benchmark.") user.press("x")
benchmark_click = benchmark() time.sleep(2)
gui.moveTo(benchmark_click[0], benchmark_click[1])
time.sleep(0.2)
gui.mouseDown()
time.sleep(0.2)
gui.mouseUp()
time.sleep(1)
user.press("down")
time.sleep(0.2)
user.press("enter")
time.sleep(0.2)
loading_screen_start = time.time() result = kerasService.wait_for_word("graphics", timeout=30)
while (True): if not result:
checkpoint() logging.info("Game didn't load to the settings menu.")
if checkpoint(): sys.exit(1)
break
elif time.time()-loading_screen_start > 360:
logging.info("Benchmark didn't start.")
exit(1)
logging.info("Benchmark hasn't started. Trying again in 10 seconds")
time.sleep(10)
t2 = time.time() logging.info("Graphics found, clicking and continuing.")
logging.info(f"Harness setup took {round((t2 - t1), 2)} seconds") gui.moveTo(result["x"], result["y"])
start_time = time.time() time.sleep(0.2)
gui.mouseDown()
time.sleep(0.2)
gui.mouseUp()
time.sleep(1)
result = kerasService.wait_for_word("benchmark", timeout=12)
if not result:
logging.info("Didn't find benchmark in settings.")
sys.exit(1)
gui.mouseDown(result["x"], result["y"])
time.sleep(0.2)
gui.mouseUp()
time.sleep(1)
user.press("down")
time.sleep(0.2)
user.press("enter")
time.sleep(0.2)
result = kerasService.wait_for_word("checkpoint", timeout=360)
if not result:
logging.info("Benchmark didn't start.")
sys.exit(1)
elapsed_setup_time = round((time.time() - setup_start_time), 2)
logging.info("Harness setup took %.2f seconds", elapsed_setup_time)
test_start_time = time.time()
time.sleep(95) # wait for benchmark to finish 95 seconds time.sleep(95) # wait for benchmark to finish 95 seconds
if results():
logging.info("Results screen found. Ending run.") result = kerasService.wait_for_word("results", timeout=25)
end_time = time.time() if not result:
logging.info(f"Benchmark took {round((end_time - start_time), 2)} seconds") logging.info("Results screen was not found!")
sys.exit(1)
test_end_time = time.time()
elapsed_test_time = round((test_end_time - test_start_time), 2)
logging.info("Benchmark took %.2f seconds", elapsed_test_time)
terminate_processes(*PROCESSES) terminate_processes(*PROCESSES)
return start_time, end_time return test_start_time, test_end_time
setup_log_directory(LOG_DIRECTORY) setup_log_directory(LOG_DIRECTORY)
@@ -168,16 +141,16 @@ kerasService = KerasService(args.keras_host, args.keras_port, os.path.join(
try: try:
start_time, end_time = run_benchmark() start_time, end_time = run_benchmark()
width, height = read_resolution(f"{CONFIG_LOCATION}\\{CONFIG_FILENAME}") width, height = read_resolution(f"{CONFIG_LOCATION}\\{CONFIG_FILENAME}")
result = { report = {
"resolution": format_resolution(width, height), "resolution": format_resolution(width, height),
"graphics_preset": "current",
"start_time": seconds_to_milliseconds(start_time), "start_time": seconds_to_milliseconds(start_time),
"end_time": seconds_to_milliseconds(end_time) "end_time": seconds_to_milliseconds(end_time)
} }
write_report_json(LOG_DIRECTORY, "report.json", result) write_report_json(LOG_DIRECTORY, "report.json", report)
#pylint: disable=broad-exception-caught
except Exception as e: except Exception as e:
logging.error("Something went wrong running the benchmark!") logging.error("Something went wrong running the benchmark!")
logging.exception(e) logging.exception(e)
terminate_processes(*PROCESSES) terminate_processes(*PROCESSES)
exit(1) sys.exit(1)

View File

@@ -1,13 +1,15 @@
"""Utility functions for Forza Horizon 5 test script"""
import re import re
def read_resolution(config_path: str) -> tuple[int]: def read_resolution(config_path: str) -> tuple[int]:
"""Gets the resolution from local file"""
height_pattern = re.compile(r"<ResolutionHeight value=\"(\d+)\"/>") height_pattern = re.compile(r"<ResolutionHeight value=\"(\d+)\"/>")
width_pattern = re.compile(r"<ResolutionWidth value=\"(\d+)\"/>") width_pattern = re.compile(r"<ResolutionWidth value=\"(\d+)\"/>")
width = 0 width = 0
height = 0 height = 0
with open(config_path) as f: with open(config_path, encoding="utf-8") as file:
lines = f.readlines() lines = file.readlines()
for line in lines: for line in lines:
height_match = height_pattern.search(line) height_match = height_pattern.search(line)
width_match = width_pattern.search(line) width_match = width_pattern.search(line)
@@ -15,4 +17,4 @@ def read_resolution(config_path: str) -> tuple[int]:
height = height_match.group(1) height = height_match.group(1)
if width_match is not None: if width_match is not None:
width = width_match.group(1) width = width_match.group(1)
return (width, height) return (width, height)