Add harnesses from latest MarkBench
72
shadowofthetombraider/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Shadow of the Tomb Raider
|
||||
|
||||
## TODO's
|
||||
- Iteration within the same game process.
|
||||
- Accept resolution as a separate argument to the harness.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- Shadow of the Tomb Raider installed.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Follow the setup instructions for the framework. If you have done so, all required python dependencies *should* be installed.
|
||||
2. Install Shadow of the Tomb Raider from steam.
|
||||
1. Location does not matter, this harness uses steam to launch the game.
|
||||
|
||||
## Configuration
|
||||
|
||||
Below is an example use of this harness as a test in a benchmark configuration.
|
||||
|
||||
```yaml
|
||||
...
|
||||
...
|
||||
tests:
|
||||
- name: shadowofthetombraider
|
||||
executable: "shadowofthetombraider.py"
|
||||
process_name: "SOTTR.exe"
|
||||
asset_paths:
|
||||
- 'harness/shadowofthetombraider/run'
|
||||
args:
|
||||
- "--preset medium"
|
||||
- "--resolution 1920,1080
|
||||
```
|
||||
|
||||
__name__ : _(required)_ name of the test. This much match the name of a directory in the harness folder so the framework
|
||||
can find the executable and any supplementary files.
|
||||
|
||||
__executable__ : _(required)_ the entry point to the test harness. In this case a python script.
|
||||
|
||||
__process_name__ : _(required)_ The process name that should be the target for FPS recording (ex: PresentMon).
|
||||
|
||||
__asset_paths__: _(optional)_ list of files to aggregate copies of after a successful test run. If a directory path is
|
||||
given, the contents are copied.
|
||||
|
||||
__args__ : _(optional)_ list of arguments to be appended to the command to execute. All the arguments will be passed to
|
||||
the executable when invoked by the framework.
|
||||
|
||||
### Arguments
|
||||
|flag|required|what?|notes
|
||||
|--|--|--|--|
|
||||
|--preset|No|Graphics preset to load for test|See the `presets` folder to determine options. If none provided, the current settings will be used|
|
||||
|--resolution|No|Display settings to load for test|If none provided, current display settings will be used|
|
||||
|
||||
#### Presets
|
||||
This harness requires a single argument for the option `preset`. This is the graphics presets that are found in the game. They are represented in YAML in the folder **presets**. To select one you take the prefix of the name of the file, and the harness will find the corresponding YAML file.
|
||||
|
||||
For example if I pass in the argument `--preset medium` to the harness. The harness will load the settings in `presets/medium.presets.yaml`. You can also create and supply a custom preset if you wish.
|
||||
|
||||
#### Resolution
|
||||
Resolution is expected to be givin in the format `height,width` so for example `1920,1080`. An error will be thrown if that format is not provided.
|
||||
|
||||
## Common Issues
|
||||
1. "Steam cannot sync with cloud"
|
||||
- A steam modal between test runs (when repeated) will come up.
|
||||
- If you are monitoring the test, you can simply manually close the modal and the test should continue normally.
|
||||
- The best solution is to disable cloud syncing for all steam games on the test bench.
|
||||
2. "Image could not be found within timeout"
|
||||
- Sometimes running the harness on a new test bench + display combination will not work right away.
|
||||
- Try a different template set, or add a new one to recitify this problem.
|
||||
- If a new template set isn't working, something else is probably bugging out.
|
||||
- This harness won't support resolutions that aren't native aspect ratios to the display.
|
||||
215
shadowofthetombraider/cv2_utils.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from enum import Enum
|
||||
|
||||
import cv2
|
||||
import imutils
|
||||
import numpy as np
|
||||
import pyautogui as gui
|
||||
import mss
|
||||
import pydirectinput as user
|
||||
|
||||
DEFAULT_MATCH_THRESHOLD = 0.9
|
||||
DEFAULT_INTERVAL = 2 # seconds
|
||||
DEFAULT_TIMEOUT = 60 # seconds
|
||||
|
||||
# path relative to script
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
images_dir = os.path.join(script_dir, "images")
|
||||
dir16x9 = os.path.join(images_dir, "16x9")
|
||||
dir16x10 = os.path.join(images_dir, "16x10")
|
||||
|
||||
templates = {
|
||||
"load_menu_play": {
|
||||
"16x9": cv2.imread(os.path.join(images_dir, "load_menu_play.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(images_dir, "load_menu_play.png"), cv2.IMREAD_UNCHANGED)
|
||||
},
|
||||
"load_menu_play_orange": {
|
||||
"16x9": cv2.imread(os.path.join(images_dir, "play_orange.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(images_dir, "play_orange.png"), cv2.IMREAD_UNCHANGED)
|
||||
},
|
||||
"menu_options": {
|
||||
"16x9": cv2.imread(os.path.join(dir16x9, "options_1080.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(dir16x10, "options.png"), cv2.IMREAD_UNCHANGED)
|
||||
},
|
||||
"menu_options_save_game": {
|
||||
"16x9": cv2.imread(os.path.join(dir16x9, "options_1080_savedgame.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(dir16x10, "options_savedgame.png"), cv2.IMREAD_UNCHANGED)
|
||||
},
|
||||
"menu_options_highlighted": {
|
||||
"16x9": cv2.imread(os.path.join(dir16x9, "options2_1080.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(dir16x10, "options2.png"), cv2.IMREAD_UNCHANGED)
|
||||
},
|
||||
"menu_options_save_game_highlighted": {
|
||||
"16x9": cv2.imread(os.path.join(dir16x9, "options2_1080_savegame.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(dir16x10, "options2_savegame.png"), cv2.IMREAD_UNCHANGED)
|
||||
},
|
||||
"menu_graphics": {
|
||||
"16x9": cv2.imread(os.path.join(dir16x9, "graphics_1080.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(dir16x10, "graphics_button.png"), cv2.IMREAD_UNCHANGED)
|
||||
},
|
||||
"menu_graphics_tab": {
|
||||
"16x9": cv2.imread(os.path.join(dir16x9, "graphics_tab_1080.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(dir16x10, "graphics_tab.png"), cv2.IMREAD_UNCHANGED)
|
||||
},
|
||||
"run_benchmark": {
|
||||
"16x9": cv2.imread(os.path.join(dir16x9, "run_1080.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(dir16x10, "run.png"), cv2.IMREAD_UNCHANGED)
|
||||
},
|
||||
"results_header": {
|
||||
"16x9": cv2.imread(os.path.join(dir16x9, "results_1080.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16x10": cv2.imread(os.path.join(dir16x10, "results.png"), cv2.IMREAD_UNCHANGED)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_template(name, set="16x9"):
|
||||
return templates[name][set]
|
||||
|
||||
|
||||
class ClickType(Enum):
|
||||
SINGLE = 0 # uses .click()
|
||||
DOUBLE = 1 # uses .doubleclick()
|
||||
HARD = 2 # uses mouse.down() and mouse.up()
|
||||
|
||||
|
||||
def get_middle_of_rect(top_left_corner, height, width):
|
||||
x = top_left_corner[0] + (width / 2)
|
||||
y = top_left_corner[1] + (height / 2)
|
||||
return int(x), int(y) # round to avoid fractional pixels
|
||||
|
||||
|
||||
def click(top_left_corner, img):
|
||||
click_loc = get_middle_of_rect(top_left_corner, img.shape[0], img.shape[1])
|
||||
logging.info(f"Clicking {click_loc}")
|
||||
user.click(click_loc[0], click_loc[1])
|
||||
|
||||
|
||||
def double_click(top_left_corner, img):
|
||||
click_loc = get_middle_of_rect(top_left_corner, img.shape[0], img.shape[1])
|
||||
logging.info(f"Double clicking {click_loc}")
|
||||
user.doubleClick(click_loc[0], click_loc[1])
|
||||
|
||||
|
||||
def hard_click(top_left_corner, img):
|
||||
click_loc = get_middle_of_rect(top_left_corner, img.shape[0], img.shape[1])
|
||||
user.moveTo(click_loc[0], click_loc[1])
|
||||
user.mouseDown()
|
||||
user.mouseUp()
|
||||
|
||||
|
||||
def wait_and_click(template_name, name, click_type: ClickType = ClickType.SINGLE, timeout=DEFAULT_TIMEOUT):
|
||||
logging.info(f"Waiting to find and click on {name}")
|
||||
img, img_loc = wait_for_image_on_screen(template_name, timeout=timeout)
|
||||
if click_type == ClickType.SINGLE:
|
||||
click(img_loc, img)
|
||||
elif click_type == ClickType.DOUBLE:
|
||||
double_click(img_loc, img)
|
||||
elif click_type == ClickType.HARD:
|
||||
hard_click(img_loc, img)
|
||||
else:
|
||||
raise ValueError("Unknown click type")
|
||||
|
||||
|
||||
class ImageNotFoundTimeout(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ImageNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def gcd(a, b):
|
||||
return a if b == 0 else gcd(b, a % b)
|
||||
|
||||
|
||||
def aspect_ratio(w, h):
|
||||
denom = int(gcd(w, h))
|
||||
x = int(w / denom)
|
||||
y = int(h / denom)
|
||||
if x == 8 and y == 5:
|
||||
return "16x10"
|
||||
elif x == 16 and y == 9:
|
||||
return "16x9"
|
||||
|
||||
|
||||
def locate_on_screen(template_name, threshold=DEFAULT_MATCH_THRESHOLD, debug=1):
|
||||
with mss.mss() as sct:
|
||||
monitor_1 = sct.monitors[1] # Identify the display to capture
|
||||
screen = np.array(sct.grab(monitor_1))
|
||||
screen = cv2.cvtColor(screen, cv2.COLOR_RGB2BGR)
|
||||
screen = cv2.cvtColor(screen, cv2.COLOR_BGR2RGB)
|
||||
(h, w) = screen.shape[:2]
|
||||
r = aspect_ratio(w, h)
|
||||
needle = get_template(template_name, r)
|
||||
return needle, locate_in_image(needle, screen, threshold=DEFAULT_MATCH_THRESHOLD, debug=0)
|
||||
|
||||
|
||||
# This approach was largely inspired by the article
|
||||
# https://pyimagesearch.com/2015/01/26/multi-scale-template-matching-using-python-opencv/
|
||||
def locate_in_image(needle, haystack, threshold=DEFAULT_MATCH_THRESHOLD, debug=0):
|
||||
(tH, tW) = needle.shape[:2]
|
||||
|
||||
if debug:
|
||||
cv2.imshow("Looking For", needle)
|
||||
cv2.waitKey(0)
|
||||
|
||||
for scale in np.linspace(0.2, 1.0, 20)[::-1]:
|
||||
# resize the image according to the scale, and keep track
|
||||
# of the ratio of the resizing
|
||||
resized = imutils.resize(haystack, width=int(haystack.shape[1] * scale), inter=cv2.INTER_NEAREST)
|
||||
r = haystack.shape[1] / float(resized.shape[1])
|
||||
|
||||
# if the resized image is smaller than the template, then break
|
||||
# from the loop
|
||||
if resized.shape[0] < tH or resized.shape[1] < tW:
|
||||
break
|
||||
|
||||
result = cv2.matchTemplate(resized, needle, cv2.TM_CCOEFF_NORMED)
|
||||
(_, maxVal, _, maxLoc) = cv2.minMaxLoc(result)
|
||||
|
||||
if debug:
|
||||
# draw a bounding box around the detected region
|
||||
# clone = np.dstack([edged, edged, edged])
|
||||
cv2.rectangle(resized, (maxLoc[0], maxLoc[1]),
|
||||
(maxLoc[0] + tW, maxLoc[1] + tH), (0, 0, 255), 2)
|
||||
cv2.imshow("Searching", resized)
|
||||
cv2.waitKey(0)
|
||||
|
||||
if maxVal >= threshold:
|
||||
found = (maxVal, maxLoc, r)
|
||||
|
||||
# unpack the bookkeeping variable and compute the (x, y) coordinates
|
||||
# of the bounding box based on the resized ratio
|
||||
(_, maxLoc, r) = found
|
||||
(startX, startY) = (int(maxLoc[0] * r), int(maxLoc[1] * r))
|
||||
(endX, endY) = (int((maxLoc[0] + tW) * r), int((maxLoc[1] + tH) * r))
|
||||
|
||||
if debug:
|
||||
# draw a bounding box around the detected result and display the image
|
||||
cv2.rectangle(haystack, (startX, startY), (endX, endY), (0, 0, 255), 2)
|
||||
cv2.imshow("Found", haystack)
|
||||
cv2.waitKey(0)
|
||||
|
||||
return (startX, startY)
|
||||
raise ImageNotFound("Image not found on screen")
|
||||
|
||||
|
||||
def wait_for_image_on_screen(template_name, match_threshold=DEFAULT_MATCH_THRESHOLD, interval=DEFAULT_INTERVAL,
|
||||
timeout=DEFAULT_TIMEOUT):
|
||||
"""Function that will wait for an image to appear on screen. This function will check every
|
||||
interval for a match that meets is greater than the match threshold. The function will raise
|
||||
an error if the image is not found within the timeout given. Will return the location
|
||||
of the image if found"""
|
||||
t0 = time.time()
|
||||
t1 = t0
|
||||
while not t1 - t0 > timeout:
|
||||
try:
|
||||
img, loc = locate_on_screen(template_name, match_threshold)
|
||||
return img, loc
|
||||
except ImageNotFound:
|
||||
pass
|
||||
time.sleep(interval)
|
||||
t1 = time.time()
|
||||
raise ImageNotFoundTimeout("Could not find image on screen within timeout")
|
||||
BIN
shadowofthetombraider/images/16x10/graphics_button.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
shadowofthetombraider/images/16x10/graphics_tab.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
shadowofthetombraider/images/16x10/options.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
shadowofthetombraider/images/16x10/options2.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
shadowofthetombraider/images/16x10/options2_savegame.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
shadowofthetombraider/images/16x10/options_savedgame.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
shadowofthetombraider/images/16x10/results.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
shadowofthetombraider/images/16x10/run.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
shadowofthetombraider/images/16x9/graphics_1080.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
shadowofthetombraider/images/16x9/graphics_tab_1080.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
shadowofthetombraider/images/16x9/options2_1080.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
shadowofthetombraider/images/16x9/options2_1080_savegame.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
shadowofthetombraider/images/16x9/options_1080.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
shadowofthetombraider/images/16x9/options_1080_savedgame.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
shadowofthetombraider/images/16x9/results_1080.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
shadowofthetombraider/images/16x9/run_1080.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
shadowofthetombraider/images/load_menu.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
shadowofthetombraider/images/load_menu_play.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
shadowofthetombraider/images/play_orange.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
shadowofthetombraider/images/tests/graphicsmenu_2k.png
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
shadowofthetombraider/images/tests/main_menu_1920x1200.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
shadowofthetombraider/images/tests/mainmenu_2k.png
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
BIN
shadowofthetombraider/images/tests/mainmenu_4k.png
Normal file
|
After Width: | Height: | Size: 14 MiB |
BIN
shadowofthetombraider/images/tests/menu1.png
Normal file
|
After Width: | Height: | Size: 14 MiB |
BIN
shadowofthetombraider/images/tests/menu2.png
Normal file
|
After Width: | Height: | Size: 12 MiB |
BIN
shadowofthetombraider/images/tests/menu3.png
Normal file
|
After Width: | Height: | Size: 12 MiB |
BIN
shadowofthetombraider/images/tests/menu4.png
Normal file
|
After Width: | Height: | Size: 12 MiB |
15
shadowofthetombraider/manifest.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
friendly_name: "Shadow of the Tomb Raider"
|
||||
executable: "shadowofthetombraider.py"
|
||||
process_name: "SOTTR.exe"
|
||||
# default recording delay to reduce capturing menus during setup, this should be revisited every test bench as loading times may be different
|
||||
recording_delay: 60
|
||||
asset_paths:
|
||||
- "harness/shadowofthetombraider/run"
|
||||
options:
|
||||
- name: preset
|
||||
type: select
|
||||
# presets disabled in favor of manual process for now
|
||||
# values: [current, low, medium, high, highest]
|
||||
values: [current]
|
||||
tooltip: Don't forget to set graphics settings!
|
||||
|
||||
2984
shadowofthetombraider/poetry.lock
generated
Normal file
48
shadowofthetombraider/presets/high.presets.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
name: high
|
||||
settings:
|
||||
- name: texture_quality
|
||||
reg_key: TextureQuality
|
||||
value: 2
|
||||
- name: texture_filtering
|
||||
reg_key: TextureFiltering
|
||||
value: 2
|
||||
- name: shadow_quality
|
||||
reg_key: ShadowQuality
|
||||
value: 2
|
||||
- name: ambient_occlusion
|
||||
reg_key: AmbientOcclusionQuality
|
||||
value: 1
|
||||
- name: depth_of_field
|
||||
reg_key: DOFQuality
|
||||
value: 1
|
||||
- name: level_of_detail
|
||||
reg_key: LevelOfDetail
|
||||
value: 1
|
||||
- name: tesselation
|
||||
reg_key: Tessellation
|
||||
value: 1
|
||||
- name: bloom
|
||||
reg_key: Bloom
|
||||
value: 1
|
||||
- name: motion_blur
|
||||
reg_key: MotionBlur
|
||||
value: 1
|
||||
- name: screen_space_reflections
|
||||
reg_key: ScreenSpaceReflections
|
||||
value: 1
|
||||
- name: screen_space_contact_shadows
|
||||
reg_key: ScreenSpaceContactShadows
|
||||
value: 0
|
||||
- name: pure_hair
|
||||
reg_key: TressFX
|
||||
value: 1
|
||||
- name: volumetric_lighting
|
||||
reg_key: VolumetricLighting
|
||||
value: 1
|
||||
- name: lens_flares
|
||||
reg_key: LensFlares
|
||||
value: 1
|
||||
- name: screen_effects
|
||||
reg_key: ScreenEffects
|
||||
value: 1
|
||||
|
||||
48
shadowofthetombraider/presets/highest.presets.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
name: highest
|
||||
settings:
|
||||
- name: texture_quality
|
||||
reg_key: TextureQuality
|
||||
value: 3
|
||||
- name: texture_filtering
|
||||
reg_key: TextureFiltering
|
||||
value: 3
|
||||
- name: shadow_quality
|
||||
reg_key: ShadowQuality
|
||||
value: 3
|
||||
- name: ambient_occlusion
|
||||
reg_key: AmbientOcclusionQuality
|
||||
value: 2
|
||||
- name: depth_of_field
|
||||
reg_key: DOFQuality
|
||||
value: 2
|
||||
- name: level_of_detail
|
||||
reg_key: LevelOfDetail
|
||||
value: 2
|
||||
- name: tesselation
|
||||
reg_key: Tessellation
|
||||
value: 1
|
||||
- name: bloom
|
||||
reg_key: Bloom
|
||||
value: 1
|
||||
- name: motion_blur
|
||||
reg_key: MotionBlur
|
||||
value: 1
|
||||
- name: screen_space_reflections
|
||||
reg_key: ScreenSpaceReflections
|
||||
value: 1
|
||||
- name: screen_space_contact_shadows
|
||||
reg_key: ScreenSpaceContactShadows
|
||||
value: 1
|
||||
- name: pure_hair
|
||||
reg_key: TressFX
|
||||
value: 1
|
||||
- name: volumetric_lighting
|
||||
reg_key: VolumetricLighting
|
||||
value: 1
|
||||
- name: lens_flares
|
||||
reg_key: LensFlares
|
||||
value: 1
|
||||
- name: screen_effects
|
||||
reg_key: ScreenEffects
|
||||
value: 1
|
||||
|
||||
48
shadowofthetombraider/presets/low.presets.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
name: low
|
||||
settings:
|
||||
- name: texture_quality
|
||||
reg_key: TextureQuality
|
||||
value: 0
|
||||
- name: texture_filtering
|
||||
reg_key: TextureFiltering
|
||||
value: 0
|
||||
- name: shadow_quality
|
||||
reg_key: ShadowQuality
|
||||
value: 1
|
||||
- name: ambient_occlusion
|
||||
reg_key: AmbientOcclusionQuality
|
||||
value: 0
|
||||
- name: depth_of_field
|
||||
reg_key: DOFQuality
|
||||
value: 0
|
||||
- name: level_of_detail
|
||||
reg_key: LevelOfDetail
|
||||
value: 0
|
||||
- name: tesselation
|
||||
reg_key: Tessellation
|
||||
value: 0
|
||||
- name: bloom
|
||||
reg_key: Bloom
|
||||
value: 1
|
||||
- name: motion_blur
|
||||
reg_key: MotionBlur
|
||||
value: 0
|
||||
- name: screen_space_reflections
|
||||
reg_key: ScreenSpaceReflections
|
||||
value: 0
|
||||
- name: screen_space_contact_shadows
|
||||
reg_key: ScreenSpaceContactShadows
|
||||
value: 0
|
||||
- name: pure_hair
|
||||
reg_key: TressFX
|
||||
value: 0
|
||||
- name: volumetric_lighting
|
||||
reg_key: VolumetricLighting
|
||||
value: 1
|
||||
- name: lens_flares
|
||||
reg_key: LensFlares
|
||||
value: 1
|
||||
- name: screen_effects
|
||||
reg_key: ScreenEffects
|
||||
value: 1
|
||||
|
||||
48
shadowofthetombraider/presets/medium.presets.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
name: medium
|
||||
settings:
|
||||
- name: texture_quality
|
||||
reg_key: TextureQuality
|
||||
value: 1
|
||||
- name: texture_filtering
|
||||
reg_key: TextureFiltering
|
||||
value: 1
|
||||
- name: shadow_quality
|
||||
reg_key: ShadowQuality
|
||||
value: 1
|
||||
- name: ambient_occlusion
|
||||
reg_key: AmbientOcclusionQuality
|
||||
value: 1
|
||||
- name: depth_of_field
|
||||
reg_key: DOFQuality
|
||||
value: 1
|
||||
- name: level_of_detail
|
||||
reg_key: LevelOfDetail
|
||||
value: 1
|
||||
- name: tesselation
|
||||
reg_key: Tessellation
|
||||
value: 0
|
||||
- name: bloom
|
||||
reg_key: Bloom
|
||||
value: 1
|
||||
- name: motion_blur
|
||||
reg_key: MotionBlur
|
||||
value: 1
|
||||
- name: screen_space_reflections
|
||||
reg_key: ScreenSpaceReflections
|
||||
value: 1
|
||||
- name: screen_space_contact_shadows
|
||||
reg_key: ScreenSpaceContactShadows
|
||||
value: 0
|
||||
- name: pure_hair
|
||||
reg_key: TressFX
|
||||
value: 1
|
||||
- name: volumetric_lighting
|
||||
reg_key: VolumetricLighting
|
||||
value: 1
|
||||
- name: lens_flares
|
||||
reg_key: LensFlares
|
||||
value: 1
|
||||
- name: screen_effects
|
||||
reg_key: ScreenEffects
|
||||
value: 1
|
||||
|
||||
21
shadowofthetombraider/pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[tool.poetry]
|
||||
name = "shadowofthetombraider-harness"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Nikolas Harris <nikolas@linusmediagroup.com"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
PyAutoGUI = "^0.9.53"
|
||||
PyDirectInput = "^1.0.4"
|
||||
opencv-python = "^4.5.5"
|
||||
Pillow = "^9.1.1"
|
||||
psutil = "^5.9.1"
|
||||
PyYAML = "^6.0"
|
||||
imutils = "^0.5.4"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
18
shadowofthetombraider/shadow_of_the_tomb_raider_utils.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import winreg
|
||||
|
||||
def get_reg(name) -> any:
|
||||
reg_path = r'SOFTWARE\Eidos Montreal\Shadow of the Tomb Raider\Graphics'
|
||||
try:
|
||||
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path, 0,
|
||||
winreg.KEY_READ)
|
||||
value, _ = winreg.QueryValueEx(registry_key, name)
|
||||
winreg.CloseKey(registry_key)
|
||||
return value
|
||||
except WindowsError:
|
||||
return None
|
||||
|
||||
|
||||
def get_resolution() -> tuple[int]:
|
||||
width = get_reg("FullscreenWidth")
|
||||
height = get_reg("FullscreenHeight")
|
||||
return (height, width)
|
||||
117
shadowofthetombraider/shadowofthetombraider.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from subprocess import Popen
|
||||
import sys
|
||||
|
||||
from cv2_utils import *
|
||||
from shadow_of_the_tomb_raider_utils import get_resolution
|
||||
|
||||
sys.path.insert(1, os.path.join(sys.path[0], '..'))
|
||||
|
||||
from harness_utils.logging import *
|
||||
from harness_utils.process import terminate_processes
|
||||
from harness_utils.steam import get_run_game_id_command, DEFAULT_EXECUTABLE_PATH as steam_path
|
||||
|
||||
STEAM_GAME_ID = 750920
|
||||
SCRIPT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
|
||||
LOG_DIRECTORY = os.path.join(SCRIPT_DIRECTORY, "run")
|
||||
|
||||
|
||||
def start_game():
|
||||
steam_run_arg = get_run_game_id_command(STEAM_GAME_ID)
|
||||
logging.info(steam_path + " " + steam_run_arg)
|
||||
return Popen([steam_path, steam_run_arg])
|
||||
|
||||
|
||||
def clickOptions(options):
|
||||
"""
|
||||
If the game is freshly installed the main menu has less options thus the options button
|
||||
looks different. We also account for if the options button is highlighted if the mouse
|
||||
is hovering over it.
|
||||
"""
|
||||
if options is None or len(options) == 0:
|
||||
return
|
||||
|
||||
try:
|
||||
wait_and_click(options[0], "graphics options", ClickType.HARD)
|
||||
except:
|
||||
clickOptions(options[1:])
|
||||
|
||||
|
||||
def run_benchmark():
|
||||
"""
|
||||
Start game via Steam and enter fullscreen mode
|
||||
"""
|
||||
t1 = time.time()
|
||||
game_process = start_game()
|
||||
|
||||
try:
|
||||
wait_and_click('load_menu_play', "play button", timeout=30)
|
||||
except:
|
||||
wait_and_click('load_menu_play_orange', "play button", timeout=30)
|
||||
|
||||
"""
|
||||
Wait for game to load and enter graphics submenu
|
||||
"""
|
||||
optionImages = [
|
||||
'menu_options_save_game',
|
||||
'menu_options_save_game_highlighted',
|
||||
'menu_options',
|
||||
'menu_options_highlighted',
|
||||
]
|
||||
clickOptions(optionImages)
|
||||
wait_and_click('menu_graphics', "graphics options", ClickType.HARD)
|
||||
time.sleep(2) # let the menu transition
|
||||
screen = gui.screenshot(os.path.join(LOG_DIRECTORY, "display_settings.png"))
|
||||
wait_and_click('menu_graphics_tab', "graphics tab", ClickType.HARD)
|
||||
screen = gui.screenshot(os.path.join(LOG_DIRECTORY, "graphics_settings.png"))
|
||||
|
||||
"""
|
||||
Start the benchmark!
|
||||
"""
|
||||
t2 = time.time()
|
||||
logging.info(f"Harness setup took {round((t2 - t1), 2)} seconds")
|
||||
user.press("r")
|
||||
start_time = time.time()
|
||||
|
||||
"""
|
||||
Wait for benchmark to complete
|
||||
"""
|
||||
time.sleep(180)
|
||||
wait_for_image_on_screen('results_header', DEFAULT_MATCH_THRESHOLD, interval=2,
|
||||
timeout=60)
|
||||
|
||||
end_time = time.time()
|
||||
logging.info(f"Benchark took {round((end_time - start_time), 2)} seconds")
|
||||
screen = gui.screenshot(os.path.join(LOG_DIRECTORY, "results.png"))
|
||||
|
||||
# Exit
|
||||
terminate_processes("SOTTR")
|
||||
return start_time, 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)
|
||||
|
||||
try:
|
||||
start_time, end_time = run_benchmark()
|
||||
height, width = get_resolution()
|
||||
result = {
|
||||
"resolution": f"{width}x{height}",
|
||||
"graphics_preset": "current",
|
||||
"start_time": seconds_to_milliseconds(start_time),
|
||||
"end_time": seconds_to_milliseconds(end_time)
|
||||
}
|
||||
|
||||
write_report_json(LOG_DIRECTORY, "report.json", result)
|
||||
except Exception as e:
|
||||
logging.error("Something went wrong running the benchmark!")
|
||||
logging.exception(e)
|
||||
terminate_processes("SOTTR")
|
||||
exit(1)
|
||||
23
shadowofthetombraider/template_test.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import cv2
|
||||
import os
|
||||
|
||||
from cv2_utils import *
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
images_dir = os.path.join(script_dir, "images")
|
||||
test_images_dir = os.path.join(images_dir, "tests")
|
||||
|
||||
test_menus = {
|
||||
"mainmenu_2k": cv2.imread(os.path.join(test_images_dir, "mainmenu_2k.png"), cv2.IMREAD_UNCHANGED),
|
||||
"graphicsmenu_2k": cv2.imread(os.path.join(test_images_dir, "graphicsmenu_2k.png"), cv2.IMREAD_UNCHANGED),
|
||||
"mainmenu_4k": cv2.imread(os.path.join(test_images_dir, "mainmenu_4k.png"), cv2.IMREAD_UNCHANGED),
|
||||
"mainmenu_1": cv2.imread(os.path.join(test_images_dir, "menu1.png"), cv2.IMREAD_UNCHANGED),
|
||||
"mainmenu_2": cv2.imread(os.path.join(test_images_dir, "menu2.png"), cv2.IMREAD_UNCHANGED),
|
||||
"mainmenu_3": cv2.imread(os.path.join(test_images_dir, "menu3.png"), cv2.IMREAD_UNCHANGED),
|
||||
"mainmenu_4": cv2.imread(os.path.join(test_images_dir, "menu4.png"), cv2.IMREAD_UNCHANGED),
|
||||
"16:10MainMenu": cv2.imread(os.path.join(test_images_dir, "main_menu_1920x1200.png"), cv2.IMREAD_UNCHANGED)
|
||||
}
|
||||
|
||||
|
||||
found2 = locate_in_image(get_template('menu_options'), test_menus['mainmenu_4k'], threshold=0.8, debug=0)
|
||||
print(found2)
|
||||