From d906b5a4731913790da2e06ddf1477d87e9c415f Mon Sep 17 00:00:00 2001 From: Nikolas Date: Thu, 29 Aug 2024 15:32:48 -0700 Subject: [PATCH 1/5] initial draft --- cities_skylines_2/README.md | 20 +++ cities_skylines_2/citiesskylines2.py | 148 ++++++++++++++++++++ cities_skylines_2/citiesskylines2_utils.py | 102 ++++++++++++++ cities_skylines_2/config/UserState.coc | 5 + cities_skylines_2/config/continue_game.json | 6 + cities_skylines_2/manifest.yaml | 9 ++ 6 files changed, 290 insertions(+) create mode 100644 cities_skylines_2/README.md create mode 100644 cities_skylines_2/citiesskylines2.py create mode 100644 cities_skylines_2/citiesskylines2_utils.py create mode 100644 cities_skylines_2/config/UserState.coc create mode 100644 cities_skylines_2/config/continue_game.json create mode 100644 cities_skylines_2/manifest.yaml diff --git a/cities_skylines_2/README.md b/cities_skylines_2/README.md new file mode 100644 index 0000000..370acf3 --- /dev/null +++ b/cities_skylines_2/README.md @@ -0,0 +1,20 @@ +# Cities Skylines 2 +This benchmark uses a 100,000 population save at a busy intersection to see how the CPU can handle the calculations at 3x speed. It also installs a third party launcher on the system to bypass Paradox's terrible game launcher. + +## Prerequisites + +- Python 3.10+ +- Cities Skylines 2 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/cities_skylines_2/citiesskylines2.py b/cities_skylines_2/citiesskylines2.py new file mode 100644 index 0000000..892d607 --- /dev/null +++ b/cities_skylines_2/citiesskylines2.py @@ -0,0 +1,148 @@ +"""Stellaris test script""" +from argparse import ArgumentParser +import logging +import os +import time +import sys +import pyautogui as gui +import pydirectinput as user + +from citiesskylines2_utils import read_current_resolution, copy_launcherfiles, copy_launcherpath, copy_benchmarksave, copy_continuegame + +sys.path.insert(1, os.path.join(sys.path[0], '..')) + +from harness_utils.process import terminate_processes +from harness_utils.output import ( + format_resolution, + setup_log_directory, + write_report_json, + seconds_to_milliseconds, + DEFAULT_LOGGING_FORMAT, + DEFAULT_DATE_FORMAT +) +from harness_utils.steam import exec_steam_game +from harness_utils.keras_service import KerasService + +SCRIPT_DIR = Path(__file__).resolve().parent +LOG_DIR = SCRIPT_DIR.joinpath("run") +PROCESS_NAME = "cities2.exe" +STEAM_GAME_ID = 949230 +launcher_files = [ + "bootstrapper-v2.exe", + "launcher.exe", + "notlauncher-options.json" +] +save_files = [ + "Benchmark.cok", + "Benchmark.cok.cid" +] +config_files = [ + "continue_game.json", + "UserState.coc" +] + +user.FAILSAFE = False + +def setup_logging(): + """default logging config""" + LOG_DIR.mkdir(exist_ok=True) + logging.basicConfig(filename=f'{LOG_DIR}/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) + + +def start_game(): + """Launch the game with no launcher or start screen""" + return exec_steam_game(STEAM_GAME_ID) + + +def console_command(command): + """Enter a console command""" + gui.write(command) + user.press("enter") + + +def run_benchmark(keras_service): + """Starts the benchmark""" + copy_launcherfiles(launcher_files) + copy_launcherpath() + copy_benchmarksave(save_files) + copy_continuegame(config_files) + start_game() + setup_start_time = time.time() + time.sleep(14) + + result = keras_service.wait_for_word("paradox", interval=0.5, timeout=100) + if not result: + logging.info("Could not find the paused notification. Unable to mark start time!") + sys.exit(1) + user.press("esc") + user.press("esc") + user.press("esc") + time.sleep(20) + + result = keras_service.wait_for_word("grand", interval=0.5, timeout=100) + if not result: + logging.info("Could not find the paused notification. Unable to mark start time!") + sys.exit(1) + elapsed_setup_time = round(time.time() - setup_start_time, 2) + logging.info("Setup took %f seconds", elapsed_setup_time) + time.sleep(2) + logging.info('Starting benchmark') + user.press("3") + time.sleep(2) + + test_start_time = time.time() + time.sleep(180) + + test_end_time = time.time() + time.sleep(2) + user.press("1") + + # Wait 5 seconds for benchmark info + time.sleep(10) + + # End the run + elapsed_test_time = round(test_end_time - test_start_time, 2) + logging.info("Benchmark took %f seconds", elapsed_test_time) + + # Exit + terminate_processes(PROCESS_NAME) + return test_start_time, test_end_time + + +def main(): + """main entry point to the script""" + 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() + keras_service = KerasService(args.keras_host, args.keras_port, LOG_DIR.joinpath("screenshot.jpg")) + + test_start_time, test_end_time = run_benchmark(keras_service) + resolution = read_current_resolution() + report = { + "resolution": f"{resolution}", + "start_time": seconds_to_milliseconds(test_start_time), + "end_time": seconds_to_milliseconds(test_end_time) + } + + write_report_json(LOG_DIR, "report.json", report) + + +if __name__ == "__main__": + try: + setup_logging() + main() + except Exception as ex: + logging.error("Something went wrong running the benchmark!") + logging.exception(e) + terminate_processes(PROCESS_NAME) + sys.exit(1) diff --git a/cities_skylines_2/citiesskylines2_utils.py b/cities_skylines_2/citiesskylines2_utils.py new file mode 100644 index 0000000..6bea534 --- /dev/null +++ b/cities_skylines_2/citiesskylines2_utils.py @@ -0,0 +1,102 @@ +"""Utility functions for Total War: Warhammer III test script""" +import os +import re +import sys +import logging +import shutil +from pathlib import Path +import stat + +sys.path.insert(1, os.path.join(sys.path[0], '..')) + +from harness_utils.steam import get_app_install_location + +SCRIPT_DIRECTORY = Path(__file__).resolve().parent +LOG_DIRECTORY = os.path.join(SCRIPT_DIRECTORY, "run") +STEAM_GAME_ID = 949230 +LOCALAPPDATA = os.getenv("LOCALAPPDATA") +LAUNCHCONFIG_LOCATION = Path(f"{LOCALAPPDATA}\\Paradox Interactive") +INSTALL_LOCATION = Path(get_app_install_location(STEAM_GAME_ID)) +APPDATA = os.getenv("APPDATA") +CONFIG_LOCATION = Path(f"{APPDATA}\\..\\LocalLow\\Colossal Order\Cities Skylines II") +SAVE_LOCATION = Path(f"{CONFIG_LOCATION}\\Saves") +CONFIG_FILENAME = "launcher-settings.json" + + +def read_current_resolution(): + """Reads resolutions settings from local game file""" + resolution_pattern = re.compile(r"\"fullscreen_resolution\"\: \"(\d+x\d+)\"\,") + cfg = f"{CONFIG_LOCATION}\\{CONFIG_FILENAME}" + resolution = 0 + with open(cfg, encoding="utf-8") as file: + lines = file.readlines() + for line in lines: + resolution_match = resolution_pattern.search(line) + if resolution_match is not None: + resolution = resolution_match.group(1) + return resolution + + +def copy_continuegame(config_files: list[str]) -> None: + """Copy launcher files to game directory""" + for file in config_files: + try: + src_path = SCRIPT_DIRECTORY / "config" / file + CONFIG_LOCATION.mkdir(parents=True, exist_ok=True) + dest_path = CONFIG_LOCATION / file + logging.info("Copying: %s -> %s", file, dest_path) + shutil.copy(src_path, dest_path) + except OSError as err: + logging.error(f"Could not copy save information files. {err}") + raise err + +def copy_launcherfiles(launcher_files: list[str]) -> None: + """Copy launcher files to game directory""" + for file in launcher_files: + try: + src_path = SCRIPT_DIRECTORY / "launcher" / file + INSTALL_LOCATION.mkdir(parents=True, exist_ok=True) + dest_path = INSTALL_LOCATION / file + logging.info("Copying: %s -> %s", file, dest_path) + shutil.copy(src_path, dest_path) + except OSError as err: + logging.error(f"Could not copy launcher files. {err}") + raise err + +def copy_launcherpath(): + """Copy the override launcherpath file to launcherpath directory""" + try: + launcherpath = "launcherpath" + src_path = SCRIPT_DIRECTORY / "launcher" / launcherpath + LAUNCHCONFIG_LOCATION.mkdir(parents=True, exist_ok=True) + dest_path = LAUNCHCONFIG_LOCATION / launcherpath + if os.path.exists(dest_path) is True: + try: + file_path = os.path.join(LAUNCHCONFIG_LOCATION, launcherpath) + os.chmod(file_path, stat.S_IWRITE) + os.remove(file_path) + logging.info(f"Removing old launcher file from {LAUNCHCONFIG_LOCATION}") + except OSError as e: + logging.error(f"The following error occurred while trying to remove the launcherpath file: {e}.") + logging.info("Copying: %s -> %s", launcherpath, dest_path) + f = open(f"{src_path}", "w") + f.write(f"{INSTALL_LOCATION}") + f.close() + shutil.copy(src_path, dest_path) + os.chmod(dest_path, stat.S_IREAD) + except OSError as err: + logging.error(f"Could not copy the launcherpath file. {err}") + raise err + +def copy_benchmarksave(save_files: list[str]) -> None: + """Copy benchmark save file to save directory""" + for file in save_files: + try: + src_path = SCRIPT_DIRECTORY / "save" / file + SAVE_LOCATION.mkdir(parents=True, exist_ok=True) + dest_path = SAVE_LOCATION / file + logging.info("Copying: %s -> %s", file, dest_path) + shutil.copy(src_path, dest_path) + except OSError as err: + logging.error(f"Could not copy launcher files. {err}") + raise err \ No newline at end of file diff --git a/cities_skylines_2/config/UserState.coc b/cities_skylines_2/config/UserState.coc new file mode 100644 index 0000000..41356e4 --- /dev/null +++ b/cities_skylines_2/config/UserState.coc @@ -0,0 +1,5 @@ +User Settings +{ + "lastSaveGameMetadata": "4d6c3ccb7ecaebb43efcf0913bb053b0", + "naturalDisasters": false +} diff --git a/cities_skylines_2/config/continue_game.json b/cities_skylines_2/config/continue_game.json new file mode 100644 index 0000000..4ee682e --- /dev/null +++ b/cities_skylines_2/config/continue_game.json @@ -0,0 +1,6 @@ +{ + "title": "Benchmark", + "desc": "Population: 99766 Money: \u00a280542653", + "date": "2024-08-20T14:35:47", + "rawGameVersion": "1.1.7f1" +} \ No newline at end of file diff --git a/cities_skylines_2/manifest.yaml b/cities_skylines_2/manifest.yaml new file mode 100644 index 0000000..0274674 --- /dev/null +++ b/cities_skylines_2/manifest.yaml @@ -0,0 +1,9 @@ +friendly_name: "Cities Skylines II" +executable: "citiesskylines2.py" +process_name: "Cities2.exe" +output_dir: "run" +options: + - name: kerasHost + type: input + - name: kerasPort + type: input \ No newline at end of file From 1f90429313c79ef97cadaefba023c2b9e2688ee0 Mon Sep 17 00:00:00 2001 From: Nikolas Date: Fri, 30 Aug 2024 14:29:56 -0700 Subject: [PATCH 2/5] linter --- cities_skylines_2/citiesskylines2.py | 3 +-- cities_skylines_2/citiesskylines2_utils.py | 9 ++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cities_skylines_2/citiesskylines2.py b/cities_skylines_2/citiesskylines2.py index 892d607..28337e4 100644 --- a/cities_skylines_2/citiesskylines2.py +++ b/cities_skylines_2/citiesskylines2.py @@ -2,6 +2,7 @@ from argparse import ArgumentParser import logging import os +from pathlib import Path import time import sys import pyautogui as gui @@ -23,8 +24,6 @@ from harness_utils.output import ( from harness_utils.steam import exec_steam_game from harness_utils.keras_service import KerasService -SCRIPT_DIR = Path(__file__).resolve().parent -LOG_DIR = SCRIPT_DIR.joinpath("run") PROCESS_NAME = "cities2.exe" STEAM_GAME_ID = 949230 launcher_files = [ diff --git a/cities_skylines_2/citiesskylines2_utils.py b/cities_skylines_2/citiesskylines2_utils.py index 6bea534..5e2f039 100644 --- a/cities_skylines_2/citiesskylines2_utils.py +++ b/cities_skylines_2/citiesskylines2_utils.py @@ -50,6 +50,7 @@ def copy_continuegame(config_files: list[str]) -> None: logging.error(f"Could not copy save information files. {err}") raise err + def copy_launcherfiles(launcher_files: list[str]) -> None: """Copy launcher files to game directory""" for file in launcher_files: @@ -62,7 +63,8 @@ def copy_launcherfiles(launcher_files: list[str]) -> None: except OSError as err: logging.error(f"Could not copy launcher files. {err}") raise err - + + def copy_launcherpath(): """Copy the override launcherpath file to launcherpath directory""" try: @@ -87,7 +89,8 @@ def copy_launcherpath(): except OSError as err: logging.error(f"Could not copy the launcherpath file. {err}") raise err - + + def copy_benchmarksave(save_files: list[str]) -> None: """Copy benchmark save file to save directory""" for file in save_files: @@ -99,4 +102,4 @@ def copy_benchmarksave(save_files: list[str]) -> None: shutil.copy(src_path, dest_path) except OSError as err: logging.error(f"Could not copy launcher files. {err}") - raise err \ No newline at end of file + raise err From 693e80a8dcff126d803efc56415d91d86e74de65 Mon Sep 17 00:00:00 2001 From: Nikolas Date: Fri, 30 Aug 2024 14:31:42 -0700 Subject: [PATCH 3/5] linter 2 --- cities_skylines_2/citiesskylines2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cities_skylines_2/citiesskylines2.py b/cities_skylines_2/citiesskylines2.py index 28337e4..2fd83d1 100644 --- a/cities_skylines_2/citiesskylines2.py +++ b/cities_skylines_2/citiesskylines2.py @@ -24,6 +24,8 @@ from harness_utils.output import ( from harness_utils.steam import exec_steam_game from harness_utils.keras_service import KerasService +SCRIPT_DIR = Path(__file__).resolve().parent +LOG_DIR = SCRIPT_DIR.joinpath("run") PROCESS_NAME = "cities2.exe" STEAM_GAME_ID = 949230 launcher_files = [ From b3b2a1269581f8333cf1a5bd591658abb761d878 Mon Sep 17 00:00:00 2001 From: Nikolas Date: Fri, 30 Aug 2024 14:37:55 -0700 Subject: [PATCH 4/5] linter 3 --- cities_skylines_2/citiesskylines2.py | 6 ++---- cities_skylines_2/citiesskylines2_utils.py | 23 +++++++++++----------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/cities_skylines_2/citiesskylines2.py b/cities_skylines_2/citiesskylines2.py index 2fd83d1..324c9fb 100644 --- a/cities_skylines_2/citiesskylines2.py +++ b/cities_skylines_2/citiesskylines2.py @@ -14,8 +14,6 @@ sys.path.insert(1, os.path.join(sys.path[0], '..')) from harness_utils.process import terminate_processes from harness_utils.output import ( - format_resolution, - setup_log_directory, write_report_json, seconds_to_milliseconds, DEFAULT_LOGGING_FORMAT, @@ -77,7 +75,7 @@ def run_benchmark(keras_service): start_game() setup_start_time = time.time() time.sleep(14) - + result = keras_service.wait_for_word("paradox", interval=0.5, timeout=100) if not result: logging.info("Could not find the paused notification. Unable to mark start time!") @@ -144,6 +142,6 @@ if __name__ == "__main__": main() except Exception as ex: logging.error("Something went wrong running the benchmark!") - logging.exception(e) + logging.exception(ex) terminate_processes(PROCESS_NAME) sys.exit(1) diff --git a/cities_skylines_2/citiesskylines2_utils.py b/cities_skylines_2/citiesskylines2_utils.py index 5e2f039..ae9aab3 100644 --- a/cities_skylines_2/citiesskylines2_utils.py +++ b/cities_skylines_2/citiesskylines2_utils.py @@ -18,7 +18,7 @@ LOCALAPPDATA = os.getenv("LOCALAPPDATA") LAUNCHCONFIG_LOCATION = Path(f"{LOCALAPPDATA}\\Paradox Interactive") INSTALL_LOCATION = Path(get_app_install_location(STEAM_GAME_ID)) APPDATA = os.getenv("APPDATA") -CONFIG_LOCATION = Path(f"{APPDATA}\\..\\LocalLow\\Colossal Order\Cities Skylines II") +CONFIG_LOCATION = Path(f"{APPDATA}\\..\\LocalLow\\Colossal Order\\Cities Skylines II") SAVE_LOCATION = Path(f"{CONFIG_LOCATION}\\Saves") CONFIG_FILENAME = "launcher-settings.json" @@ -36,7 +36,7 @@ def read_current_resolution(): resolution = resolution_match.group(1) return resolution - + def copy_continuegame(config_files: list[str]) -> None: """Copy launcher files to game directory""" for file in config_files: @@ -47,7 +47,7 @@ def copy_continuegame(config_files: list[str]) -> None: logging.info("Copying: %s -> %s", file, dest_path) shutil.copy(src_path, dest_path) except OSError as err: - logging.error(f"Could not copy save information files. {err}") + logging.error("Could not copy save information files. %s", err) raise err @@ -61,7 +61,7 @@ def copy_launcherfiles(launcher_files: list[str]) -> None: logging.info("Copying: %s -> %s", file, dest_path) shutil.copy(src_path, dest_path) except OSError as err: - logging.error(f"Could not copy launcher files. {err}") + logging.error("Could not copy launcher files %s", err) raise err @@ -73,21 +73,20 @@ def copy_launcherpath(): LAUNCHCONFIG_LOCATION.mkdir(parents=True, exist_ok=True) dest_path = LAUNCHCONFIG_LOCATION / launcherpath if os.path.exists(dest_path) is True: - try: + try: file_path = os.path.join(LAUNCHCONFIG_LOCATION, launcherpath) os.chmod(file_path, stat.S_IWRITE) os.remove(file_path) - logging.info(f"Removing old launcher file from {LAUNCHCONFIG_LOCATION}") + logging.info("Removing old launcher file from %s", LAUNCHCONFIG_LOCATION) except OSError as e: - logging.error(f"The following error occurred while trying to remove the launcherpath file: {e}.") + logging.error("The following error occurred while trying to remove the launcherpath file: %s.", e) logging.info("Copying: %s -> %s", launcherpath, dest_path) - f = open(f"{src_path}", "w") - f.write(f"{INSTALL_LOCATION}") - f.close() + with open(f"{src_path}", "w", encoding="utf-8") as f: + f.write(f"{INSTALL_LOCATION}") shutil.copy(src_path, dest_path) os.chmod(dest_path, stat.S_IREAD) except OSError as err: - logging.error(f"Could not copy the launcherpath file. {err}") + logging.error("Could not copy the launcherpath file. %s", e) raise err @@ -101,5 +100,5 @@ def copy_benchmarksave(save_files: list[str]) -> None: logging.info("Copying: %s -> %s", file, dest_path) shutil.copy(src_path, dest_path) except OSError as err: - logging.error(f"Could not copy launcher files. {err}") + logging.error("Could not copy launcher files. %s", err) raise err From 97dc1aea240c04d0836b16afc479651134cc88d4 Mon Sep 17 00:00:00 2001 From: Nikolas Date: Fri, 30 Aug 2024 14:38:49 -0700 Subject: [PATCH 5/5] edit changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e5e2e..d9f3eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Changes are grouped by the date they are merged to the main branch of the reposi ## 2024-08-30 - Add Stellaris harness based on one_year console command. +- Add City Skylines 2 harness. ## 2024-08-29