diff --git a/.vscode/settings.json b/.vscode/settings.json index 7709c42..17c2176 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "python.linting.flake8Enabled": true, - "python.linting.enabled": true + "python.linting.enabled": true, + "python.linting.flake8Args": ["--ignore", "E111,E114,E501,E121,E722"] } \ No newline at end of file diff --git a/src/aoe2de_patcher.py b/src/aoe2de_patcher.py index ebc40b4..0fbed06 100644 --- a/src/aoe2de_patcher.py +++ b/src/aoe2de_patcher.py @@ -12,6 +12,7 @@ from logic import Logic from utils import base_path + class App(): def __init__(self): self.logic = Logic() @@ -31,11 +32,11 @@ def on_closing(): # Log text box content to file with open(base_path() / "log.txt", "w+") as file: file.write(self.text_box.get(1.0, "end-1c")) - + self.window.destroy() self.window.protocol("WM_DELETE_WINDOW", on_closing) - + self.upper_frame = tk.Frame(master=self.window) self.upper_frame.pack(side="top", expand=True, fill="both", padx=10, pady=(10, 5)) self.upper_frame.columnconfigure(0, weight=1) @@ -51,14 +52,14 @@ def on_closing(): self.lower_frame = tk.Frame(master=self.window) self.lower_frame.pack(side="bottom", expand=True, fill="both", padx=10, pady=(5, 10)) - self.selected_patch_title = tk.StringVar() - + self.selected_patch_title = tk.StringVar() + patch_titles = [f"{p['version']} - {time.strftime('%d/%m/%Y', time.gmtime(p['date']))}" for p in self.patch_list] self.lbl_select_patch = ttk.Label(master=self.upper_frame, text="Target version") - self.lbl_select_patch.grid(row=0, column=0, sticky="e") + self.lbl_select_patch.grid(row=0, column=0, sticky="e") self.cmb_select_patch = ttk.Combobox(self.upper_frame, state="readonly", textvariable=self.selected_patch_title, values=[p for p in patch_titles]) - self.cmb_select_patch.current(0) # Set default value + self.cmb_select_patch.current(0) # Set default value self.cmb_select_patch.grid(row=0, column=1, sticky="w") self.btn_patch = ttk.Button(master=self.upper_frame, text="Patch", command=self._patch) @@ -129,7 +130,7 @@ def work(): self._disable_input() self.logic.restore() self._enable_input() - + t = threading.Thread(target=work) t.start() @@ -153,6 +154,7 @@ def _enable_input(self): self.ent_username.config(state="enabled") self.ent_password.config(state="enabled") + if __name__ == '__main__': app = App() - app.start() \ No newline at end of file + app.start() diff --git a/src/logic.py b/src/logic.py index 020749b..193939a 100644 --- a/src/logic.py +++ b/src/logic.py @@ -5,7 +5,6 @@ import signal import tempfile import re -import time from queue import Queue from dataclasses import dataclass @@ -17,6 +16,7 @@ from webhook import Webhook import utils + @dataclass class Manifest(): depot: int @@ -28,14 +28,14 @@ class Manifest(): size_compressed: int files: list + class Logic: app_id = 813780 def __init__(self): self.webhook = Webhook() # The earliest patch that works was released after directx update - # @TODO Try to figure out a way to patch to earlier patches than this - #self.directx_update_date = time.struct_time((2020, 2, 17, 0, 0, 0, 0, 48, 0)) + # @TODO Try to figure out a way to patch to earlier patches than this: time.struct_time((2020, 2, 17, 0, 0, 0, 0, 48, 0)) self.download_dir = utils.base_path() / "download" self.manifest_dir = utils.base_path() / "manifests" self.backup_dir = utils.base_path() / "backup" @@ -166,7 +166,7 @@ def cancel_downloads(self): for process in self.process_queue.queue: process.kill(signal.SIGTERM) - def _download_patch(self, username: str, password: str, target_version: int): + def _download_patch(self, username: str, password: str, target_version: int): """Download the given patch using the steam account credentials. Args: @@ -182,8 +182,8 @@ def _download_patch(self, username: str, password: str, target_version: int): if not (utils.check_dotnet()): print("DOTNET Core required but not found!") return False - - update_list = [] + + update_list = [] tmp_files = [] # Remove previous download folder if it exists @@ -237,7 +237,7 @@ def _download_patch(self, username: str, password: str, target_version: int): changes = self._get_filelist(username, password, depot_id, current_manifest_id, target_manifest_id) # Files have changed, store changes to temp file and add to update list - if not changes is None: + if changes is not None: # Create temp file tmp = tempfile.NamedTemporaryFile(mode="w", delete=False) @@ -249,12 +249,12 @@ def _download_patch(self, username: str, password: str, target_version: int): tmp.close() # Add update element to list - update_list.append({ 'depot_id': depot_id, 'manifest_id': target_manifest_id, 'filelist': tmp.name }) + update_list.append({'depot_id': depot_id, 'manifest_id': target_manifest_id, 'filelist': tmp.name}) else: print(f"Depot ID not matching, discarding pair ({current_depot['depot_id']}, {target_depot['depot_id']})") - + print("Downloading files") - + # Loop all necessary updates for element in update_list: # Stop if a download didn't succeed @@ -323,7 +323,7 @@ def _depot_downloader(self, options: list): depot_downloader_path = f"\"{str(utils.resource_path('DepotDownloader/DepotDownloader.dll').absolute())}\"" args = ["dotnet", depot_downloader_path] + options - + # Spawn process and store in queue p = pexpect.popen_spawn.PopenSpawn(" ".join(args), encoding="utf-8") self.process_queue.put(p) @@ -345,7 +345,7 @@ def _depot_downloader(self, options: list): success = True # Code required - elif response == 1: + elif response == 1: # Open popup for 2FA Code # Create temporary parent window to prevent error with visibility temp = tkinter.Tk() @@ -374,7 +374,7 @@ def _depot_downloader(self, options: list): # Wait for program to finish p.expect(pexpect.EOF, timeout=None) - except pexpect.exceptions.TIMEOUT as e: + except pexpect.exceptions.TIMEOUT: print("Error waiting for DepotDownloader to start") except ConnectionError as e: print(e) @@ -399,11 +399,11 @@ def _download_manifest(self, username: str, password: str, depot_id: int, manife Returns: bool: True if successful """ - args = ["-app", str(self.app_id), - "-depot", str(depot_id), - "-manifest", str(manifest_id), - "-username", username, - "-password", password, + args = ["-app", str(self.app_id), + "-depot", str(depot_id), + "-manifest", str(manifest_id), + "-username", username, + "-password", password, "-remember-password", "-dir", str(self.manifest_dir), "-manifest-only"] @@ -426,11 +426,11 @@ def _download_depot(self, username: str, password: str, depot_id: int, manifest_ Returns: bool: True if successful """ - args = ["-app", str(self.app_id), - "-depot", str(depot_id), - "-manifest", str(manifest_id), - "-username", username, - "-password", password, + args = ["-app", str(self.app_id), + "-depot", str(depot_id), + "-manifest", str(manifest_id), + "-username", username, + "-password", password, "-remember-password", "-dir", str(self.download_dir), "-filelist", filelist] @@ -476,7 +476,7 @@ def _get_filelist(self, username: str, password: str, depot_id: int, current_man # Find all added files (Result contains added files and files with different hash) diff_added = list(current_set.difference(target_set)) diff_added_names = set([x[0] for x in diff_added]) - + # Find all removed files (Remove files with same name but different hash) removed = set.difference(diff_removed_names, diff_added_names) @@ -531,7 +531,8 @@ def _read_manifest(self, file: pathlib.Path): size_compressed = 0 files = [] - line = f.readline() # First line contains depot id + # First line contains depot id + line = f.readline() depot = re.match(r".* (\d+)", line).groups()[0] # Second lines is empty @@ -540,9 +541,9 @@ def _read_manifest(self, file: pathlib.Path): line = f.readline() groups = re.match(r".* : (\d+) \/ (.+)", line).groups() id = groups[0] - date = groups[1] # (Temporary) workaround since date isn't used anyways. + date = groups[1] # (Temporary) workaround since date isn't used anyways. # Date format seems to be localized... @TODO find a way to universally parse datestring - #date = time.mktime(time.strptime(groups[1], "%d.%m.%Y %H:%M:%S")) + # date = time.mktime(time.strptime(groups[1], "%d.%m.%Y %H:%M:%S")) # Fourth line contains number of files line = f.readline() @@ -585,4 +586,4 @@ def _get_game_version(self): """ metadata = utils.get_version_number(self.game_dir / "AoE2DE_s.exe") - return (metadata[1] - 101) * 65536 + metadata[2] \ No newline at end of file + return (metadata[1] - 101) * 65536 + metadata[2] diff --git a/src/redirector.py b/src/redirector.py index 98c9159..0199468 100644 --- a/src/redirector.py +++ b/src/redirector.py @@ -1,13 +1,15 @@ import utils + class IORedirector(object): def __init__(self, text_widget): # Store the widget self.text_widget = text_widget + class StdoutRedirector(IORedirector): def write(self, text): utils.log(self.text_widget, text) def flush(self): - pass \ No newline at end of file + pass diff --git a/src/utils.py b/src/utils.py index 9c94418..86cd8a6 100644 --- a/src/utils.py +++ b/src/utils.py @@ -5,7 +5,8 @@ from tkinter import Text -def get_version_number (path: pathlib.Path): + +def get_version_number(path: pathlib.Path): """Retrieve the version number of a binary file. Args: @@ -29,6 +30,7 @@ def get_version_number (path: pathlib.Path): version_number = tuple(int(x) for x in proc.stdout.read().strip().split()) return version_number + def log(text_widget: Text, text: str): """Logs a given string to the text widget. @@ -41,6 +43,7 @@ def log(text_widget: Text, text: str): text_widget.configure(state="disabled") text_widget.see("end") + def copy_file_or_dir(source_dir: pathlib.Path, target_dir: pathlib.Path, file: str): """Copies a file or a directory recursively into the target directory. @@ -54,6 +57,7 @@ def copy_file_or_dir(source_dir: pathlib.Path, target_dir: pathlib.Path, file: s else: shutil.copy((source_dir / file).absolute(), (target_dir / file).absolute()) + def remove_file_or_dir(path: pathlib.Path): """Removes a file or directory recursively. Does not throw an error if file does not exist. @@ -65,6 +69,7 @@ def remove_file_or_dir(path: pathlib.Path): else: path.unlink(missing_ok=True) + def backup_files(original_dir: pathlib.Path, override_dir: pathlib.Path, backup_dir: pathlib.Path, debug_info: bool): """Recursively performs backup of original_dir to backup_dir assuming all files/folder from override_dir will be patched. @@ -85,9 +90,10 @@ def backup_files(original_dir: pathlib.Path, override_dir: pathlib.Path, backup_ else: if debug_info: print(f"Copy {(original_dir / file).absolute()}") - + copy_file_or_dir(original_dir, backup_dir, file) + def remove_patched_files(original_dir: pathlib.Path, override_dir: pathlib.Path, debug_info: bool): """Recursively removes all patched files assuming original_dir has been patched with all files from override_dir. @@ -104,7 +110,7 @@ def remove_patched_files(original_dir: pathlib.Path, override_dir: pathlib.Path, # Remove all overridden files try: for file in changed_file_list: - # Its a folder, remove its contents + # Its a folder, remove its contents if (original_dir / file).is_dir(): remove_patched_files(original_dir / file, override_dir / file, debug_info) @@ -118,11 +124,12 @@ def remove_patched_files(original_dir: pathlib.Path, override_dir: pathlib.Path, else: if debug_info: print(f"Remove {(original_dir / file).absolute()}") - + remove_file_or_dir(original_dir / file) except BaseException as e: raise e + def check_dotnet(): """Checks if dotnet is available. @@ -131,12 +138,13 @@ def check_dotnet(): """ return not (shutil.which("dotnet") is None) + def base_path(): """Construct the base path to the exe / project. Returns: pathlib.Path: The base path of the exectuable or project - """ + """ # Get absolute path to resource, works for dev and for PyInstaller if getattr(sys, 'frozen', False): # PyInstaller creates a temp folder and stores path in _MEIPASS @@ -144,6 +152,7 @@ def base_path(): else: return pathlib.Path() + def resource_path(relative_path: str): """Construct the resource patch for a resource. @@ -162,7 +171,8 @@ def resource_path(relative_path: str): return base_path / "res" / relative_path -def clear(): + +def clear(): """Clear the screen of the console. """ - _ = os.system('cls') \ No newline at end of file + _ = os.system('cls') diff --git a/src/utils_win32.py b/src/utils_win32.py index bf2aac8..68adab9 100644 --- a/src/utils_win32.py +++ b/src/utils_win32.py @@ -1,7 +1,8 @@ import sys import win32api -def get_version_number_win32 (path: str): + +def get_version_number_win32(path: str): """Retrieve the version number of a binary file. Args: @@ -13,9 +14,10 @@ def get_version_number_win32 (path: str): info = win32api.GetFileVersionInfo(path, "\\") ms = info['FileVersionMS'] ls = info['FileVersionLS'] - return win32api.HIWORD (ms), win32api.LOWORD (ms), win32api.HIWORD (ls), win32api.LOWORD (ls) + return win32api.HIWORD(ms), win32api.LOWORD(ms), win32api.HIWORD(ls), win32api.LOWORD(ls) + if __name__ == '__main__': path = sys.argv[1] version = get_version_number_win32(path) - sys.stdout.write(" ".join([str(x) for x in version])+"\n") + sys.stdout.write(" ".join([str(x) for x in version]) + "\n") diff --git a/src/webhook.py b/src/webhook.py index 73368bd..0b3d5e0 100644 --- a/src/webhook.py +++ b/src/webhook.py @@ -3,6 +3,7 @@ import requests + class Webhook: def query_latest_version(self): """Returns the latest version of the patch tool. @@ -26,10 +27,10 @@ def query_patches(self): response = self._query_website(url) result = json.loads(response.content)["patches"] - + return result - - def _query_website(self, url: str, headers: dict=None, ignore_success: bool=False): + + def _query_website(self, url: str, headers: dict = None, ignore_success: bool = False): """Query a website with the given headers. Args: @@ -67,4 +68,4 @@ def _print_response_error(self, response: requests.Response): Args: response (requests.Response): The response containing the error code """ - print(f"Error in HTML request: {response.status_code} ({response.url})") \ No newline at end of file + print(f"Error in HTML request: {response.status_code} ({response.url})")