From d906b5a4731913790da2e06ddf1477d87e9c415f Mon Sep 17 00:00:00 2001 From: Nikolas Date: Thu, 29 Aug 2024 15:32:48 -0700 Subject: [PATCH] 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