diff --git a/Pack/MKWFaraphel/file/SuperMario256.ttf b/assets/SuperMario256.ttf similarity index 100% rename from Pack/MKWFaraphel/file/SuperMario256.ttf rename to assets/SuperMario256.ttf diff --git a/source/CT_Config.py b/source/CT_Config.py index d3c176a..4992fdd 100644 --- a/source/CT_Config.py +++ b/source/CT_Config.py @@ -7,28 +7,6 @@ from source.Cup import Cup from source.Track import Track, HiddenTrackAttr -def get_cup_icon(cup_id: [str, int], font_path: str = "./file/SuperMario256.ttf", - cup_icon_dir: str = "./file/cup_icon") -> Image: - """ - :param cup_id: id of the cup - :param cup_icon_dir: directory to cup icon - :param font_path: path to the font used to generate icon - :return: cup icon - """ - if os.path.exists(f"{cup_icon_dir}/{cup_id}.png"): - cup_icon = Image.open(f"{cup_icon_dir}/{cup_id}.png").resize((128, 128)) - - else: - cup_icon = Image.new("RGBA", (128, 128)) - draw = ImageDraw.Draw(cup_icon) - font = ImageFont.truetype(font_path, 90) - draw.text((4, 4), "CT", (255, 165, 0), font=font, stroke_width=2, stroke_fill=(0, 0, 0)) - font = ImageFont.truetype(font_path, 60) - draw.text((5, 80), "%03i" % cup_id, (255, 165, 0), font=font, stroke_width=2, stroke_fill=(0, 0, 0)) - - return cup_icon - - class CT_Config: def __init__(self, version: str = None, name: str = None, nickname: str = None, game_variant: str = "01", gui=None, region: int = None, cheat_region: int = None, @@ -117,6 +95,27 @@ class CT_Config: ctfile.write(cup.get_ctfile_cup(race=False, **kwargs)) rctfile.write(cup.get_ctfile_cup(race=True, **kwargs)) + def get_cup_icon(self, cup_id: [str, int], font_path: str = "./assets/SuperMario256.ttf", + cup_icon_dir: str = "./file/cup_icon") -> Image: + """ + :param cup_id: id of the cup + :param cup_icon_dir: directory to cup icon + :param font_path: path to the font used to generate icon + :return: cup icon + """ + if os.path.exists(f"{cup_icon_dir}/{cup_id}.png"): + cup_icon = Image.open(f"{cup_icon_dir}/{cup_id}.png").resize((128, 128)) + + else: + cup_icon = Image.new("RGBA", (128, 128)) + draw = ImageDraw.Draw(cup_icon) + font = ImageFont.truetype(font_path, 90) + draw.text((4, 4), "CT", (255, 165, 0), font=font, stroke_width=2, stroke_fill=(0, 0, 0)) + font = ImageFont.truetype(font_path, 60) + draw.text((5, 80), "%03i" % cup_id, (255, 165, 0), font=font, stroke_width=2, stroke_fill=(0, 0, 0)) + + return cup_icon + def get_cticon(self) -> Image: """ get all cup icon into a single image @@ -133,7 +132,7 @@ class CT_Config: for index, cup_id in enumerate(icon_files): # index is a number, id can be string or number ("left", 0, 12, ...) - cup_icon = get_cup_icon(cup_id) + cup_icon = self.get_cup_icon(cup_id, cup_icon_dir=self.pack_path+"/file/cup_icon/") ct_icon.paste(cup_icon, (0, index * CT_ICON_WIDTH)) return ct_icon @@ -159,19 +158,20 @@ class CT_Config: self.unordered_tracks = [] self.all_tracks = [] + self.pack_path = pack_path + # default track - self.default_track = Track() + self.default_track = Track(track_wu8_dir=f"{self.pack_path}/file/Track-WU8/") if "default_track" in ctconfig_json: self.default_track.load_from_json(ctconfig_json["default_track"]) for cup_json in ctconfig_json["cup"] if "cup" in ctconfig_json else []: # tracks with defined order cup = Cup(default_track=self.default_track) cup.load_from_json(cup_json) - if not cup.locked: # locked cup are not useful (they are original track or random track) - self.ordered_cups.append(cup) - self.all_tracks.extend(cup.tracks) + self.ordered_cups.append(cup) + self.all_tracks.extend(cup.tracks) for track_json in ctconfig_json["tracks_list"] if "tracks_list" in ctconfig_json else []: # unordered tracks - track = Track() + track = Track(track_wu8_dir=f"{self.pack_path}/file/Track-WU8/") track.load_from_json(track_json) self.unordered_tracks.append(track) self.all_tracks.append(track) @@ -182,7 +182,7 @@ class CT_Config: self.nickname = ctconfig_json["nickname"] if "nickname" in ctconfig_json else self.name if "game_variant" in ctconfig_json: self.game_variant = ctconfig_json["game_variant"] - for param in ["region", "cheat_region", "tags_color", "prefix_list", "suffix_list", "tag_retro", "pack_path"]: + for param in ["region", "cheat_region", "tags_color", "prefix_list", "suffix_list", "tag_retro"]: setattr(self, param, ctconfig_json.get(param)) return self diff --git a/source/Cup.py b/source/Cup.py index 265fff2..99b83b7 100644 --- a/source/Cup.py +++ b/source/Cup.py @@ -9,7 +9,6 @@ class Cup: track2: Track = None, track3: Track = None, track4: Track = None, - locked: bool = False, *args, **kwargs): """ class of a cup @@ -18,13 +17,11 @@ class Cup: :param track2: second track :param track3: third track :param track4: fourth track - :param locked: is the track locked (used to load ctconfig in CT_Config) :param args: other args that I could add in the future :param kwargs: other kwargs that I could add in the future """ self.name = name - self.locked = locked self.tracks = [ track1 if track1 else default_track.copy(), track2 if track2 else default_track.copy(), @@ -51,7 +48,7 @@ class Cup: for key, value in cup.items(): # load all value in the json as class attribute if key == "tracks": # if the key is tracks for i, track_json in enumerate(value): # load all tracks from their json - self.tracks[int(i)].load_from_json(track_json) + self.tracks[i].load_from_json(track_json) else: setattr(self, key, value) diff --git a/source/Game.py b/source/Game.py index cc53bf9..59b7dfa 100644 --- a/source/Game.py +++ b/source/Game.py @@ -213,14 +213,14 @@ class Game: szs_extract_path = path + ".d" if os.path.exists(szs_extract_path + subpath): if subpath[-1] == "/": - shutil.copyfile(f"./file/{file}", szs_extract_path + subpath + file) + shutil.copyfile(f"{self.ctconfig.pack_path}/file/{file}", szs_extract_path + subpath + file) else: - shutil.copyfile(f"./file/{file}", szs_extract_path + subpath) + shutil.copyfile(f"{self.ctconfig.pack_path}/file/{file}", szs_extract_path + subpath) elif path[-1] == "/": - shutil.copyfile(f"./file/{file}", path + file) + shutil.copyfile(f"{self.ctconfig.pack_path}/file/{file}", path + file) else: - shutil.copyfile(f"./file/{file}", path) + shutil.copyfile(f"{self.ctconfig.pack_path}/file/{file}", path) for fp in fs: for f in glob.glob(self.path + "/files/" + fp, recursive=True): @@ -282,10 +282,11 @@ class Game: """ self.gui.progress(statut=self.gui.translate("Patch lecode.bin"), add=1) - lpar_path = "./file/lpar-debug.txt" if self.gui.boolvar_use_debug_mode.get() else "./file/lpar-default.txt" + lpar_path = f"{self.ctconfig.pack_path}/file/lpar-debug.txt" \ + if self.gui.boolvar_use_debug_mode.get() else f"{self.ctconfig.pack_path}/file/lpar-default.txt" lec.patch( - lecode_file=f"./file/lecode-{self.region}.bin", + lecode_file=f"{self.ctconfig.pack_path}/file/lecode-{self.region}.bin", dest_lecode_file=f"{self.path}/files/rel/lecode-{self.region}.bin", game_track_path=f"{self.path}/files/Race/Course/", copy_track_paths=[f"./file/Track/"], @@ -422,13 +423,12 @@ class Game: :param bmg_language: language of the bmg file :return: the replaced bmg file """ - with open("./file_process.json", encoding="utf8") as fp_file: + with open(f"{self.ctconfig.pack_path}/file_process.json", encoding="utf8") as fp_file: file_process = json.load(fp_file) for bmg_process in file_process["bmg"]: - if "language" in bmg_process: - if bmg_language not in bmg_process["language"]: - continue + if "language" in bmg_process and bmg_language not in bmg_process["language"]: + continue for data, data_replacement in bmg_process["data"].items(): for key, replacement in bmg_replacement.items(): @@ -462,17 +462,18 @@ class Game: bmg.encode(file) os.remove(file) - save_bmg(f"./file/Menu_{bmglang}.txt", process_bmg_replacement(bmgmenu, bmglang)) - save_bmg(f"./file/Common_{bmglang}.txt", process_bmg_replacement(bmgcommon, bmglang)) - save_bmg(f"./file/Common_R{bmglang}.txt", process_bmg_replacement(rbmgcommon, bmglang)) + save_bmg(f"{self.ctconfig.pack_path}/file/Menu_{bmglang}.txt", process_bmg_replacement(bmgmenu, bmglang)) + save_bmg(f"{self.ctconfig.pack_path}/file/Common_{bmglang}.txt", process_bmg_replacement(bmgcommon, bmglang)) + save_bmg(f"{self.ctconfig.pack_path}/file/Common_R{bmglang}.txt", process_bmg_replacement(rbmgcommon, bmglang)) def patch_file(self): """ Prepare all files to install the mod (track, bmg text, descriptive image, ...) """ try: - if not (os.path.exists("./file/Track-WU8/")): os.makedirs("./file/Track-WU8/") - with open("./file_process.json", encoding="utf8") as fp_file: + os.makedirs(f"{self.ctconfig.pack_path}/file/Track-WU8/", exist_ok=True) + + with open(f"{self.ctconfig.pack_path}/file_process.json", encoding="utf8") as fp_file: file_process = json.load(fp_file) max_step = len(file_process["img"]) + len(self.ctconfig.all_tracks) + 3 + len("EGFIS") @@ -486,10 +487,13 @@ class Game: self.gui.progress(statut=self.gui.translate("Creating ct_icon.png"), add=1) ct_icon = self.ctconfig.get_cticon() - ct_icon.save("./file/ct_icons.tpl.png") + ct_icon.save(f"{self.ctconfig.pack_path}/file/ct_icons.tpl.png") self.gui.progress(statut=self.gui.translate("Creating descriptive images"), add=1) - self.patch_img_desc() + self.patch_img_desc( + img_desc_path=self.ctconfig.pack_path+"/file/img_desc/", + dest_dir=self.ctconfig.pack_path+"/file/" + ) self.patch_image(file_process["img"]) for file in glob.glob(self.path + "/files/Scene/UI/MenuSingle_?.szs"): self.patch_bmg(file) # MenuSingle could be any other file, Common and Menu are all the same in all other files. @@ -511,7 +515,12 @@ class Game: for i, file in enumerate(fp_img): self.gui.progress(statut=self.gui.translate("Converting images") + f"\n({i + 1}/{len(fp_img)}) {file}", add=1) - img.encode(file="./file/" + file, format=fp_img[file]) + # TODO: IMG DESC AND THIS PART REALLY NEED A REWRITE ! + + img.encode( + file=f"{self.ctconfig.pack_path}/file/{file}", + format=fp_img[file] + ) def patch_img_desc(self, img_desc_path: str = "./file/img_desc/", dest_dir: str = "./file/") -> None: """ diff --git a/source/Gui.py b/source/Gui.py index dc9a9b1..757ce8c 100644 --- a/source/Gui.py +++ b/source/Gui.py @@ -7,7 +7,6 @@ import requests import zipfile import glob import json -import os from source.Game import Game, RomAlreadyPatched, InvalidGamePath, InvalidFormat from source.Option import Option @@ -25,9 +24,10 @@ class Gui: Initialize program Gui """ self.root = Tk() + self.root.resizable(False, False) + self.root.iconbitmap(bitmap="./icon.ico") - self.option = Option() - self.option.load_from_file("./option.json") + self.option = Option().load_from_file("./option.json") self.game = Game(gui=self) self.menu_bar = None @@ -47,42 +47,35 @@ class Gui: self.boolvar_dont_check_for_update = BooleanVar(value=self.option.dont_check_for_update) self.intvar_process_track = IntVar(value=self.option.process_track) + self.root.title(self.translate("MKWFaraphel Installer")) + self.boolvar_use_1star_track = BooleanVar(value=True) self.boolvar_use_2star_track = BooleanVar(value=True) self.boolvar_use_3star_track = BooleanVar(value=True) - self.stringvar_mark_track_from_version = StringVar(value="None") self.stringvar_sort_track_by = StringVar(value="name") + self.boolvar_use_debug_mode = BooleanVar(value=False) self.boolvar_force_unofficial_mode = BooleanVar(value=False) self.stringvar_mystuff_folder = StringVar(value=None) - self.stringvar_mystuff_music_folder = StringVar(value=None) - self.stringvar_mystuff_vehicle_folder = StringVar(value=None) - self.stringvar_mystuff_character_folder = StringVar(value=None) - self.stringvar_mystuff_original_track_folder = StringVar(value=None) - - self.root.title(self.translate("MKWFaraphel Installer")) - self.root.resizable(False, False) - self.root.iconbitmap(bitmap="./icon.ico") if not self.boolvar_dont_check_for_update.get(): self.check_update() - self.init_gui() - self.init_menu() - - def init_gui(self) -> None: + # GUI + # Mod selector self.frame_ctconfig = LabelFrame(self.root, text=self.translate("Mod")) self.frame_ctconfig.grid(row=1, column=1, sticky="NWS") self.combobox_ctconfig_path = ttk.Combobox( self.frame_ctconfig, values=self.available_packs, - textvariable=self.stringvar_ctconfig + textvariable=self.stringvar_ctconfig, + width=30 ) self.combobox_ctconfig_path.grid(row=1, column=1, sticky="NEWS", columnspan=2) - self.combobox_ctconfig_path.bind("<>", lambda x=None: self.init_menu()) - + self.combobox_ctconfig_path.bind("<>", lambda x=None: self.reload_ctconfig()) + self.reload_ctconfig() # Jeu self.frame_game_path = LabelFrame(self.root, text=self.translate("Original game")) @@ -136,21 +129,22 @@ class Gui: self.game.patch_file() self.game.install_mod() - self.button_do_everything = Button(self.frame_game_path_action, text=self.translate("Install mod"), - relief=RIDGE, command=do_everything) + self.button_do_everything = Button( + self.frame_game_path_action, + text=self.translate("Install mod"), + relief=RIDGE, + command=do_everything + ) self.button_do_everything.grid(row=1, column=1, columnspan=2, sticky="NEWS") self.progressbar = ttk.Progressbar(self.root) self.progresslabel = Label(self.root) - def init_menu(self) -> None: + if self.menu_bar: self.menu_bar.destroy() self.menu_bar = Menu(self.root) self.root.config(menu=self.menu_bar) - self.game.ctconfig.load_ctconfig_file(ctconfig_file=self.get_ctconfig_path_pack(self.stringvar_ctconfig.get())) - track_attr_possibilities = self.game.ctconfig.get_all_track_possibilities() - # LANGUAGE MENU self.menu_language = Menu(self.menu_bar, tearoff=0) self.menu_bar.add_cascade(label=self.translate("Language"), menu=self.menu_language) @@ -195,50 +189,6 @@ class Gui: command=lambda: self.option.edit("format", "WBFS") ) - # TRACK CONFIGURATION MENU - self.menu_trackconfiguration = Menu(self.menu_bar, tearoff=0) - self.menu_bar.add_cascade(label=self.translate("Track configuration"), menu=self.menu_trackconfiguration) - - # sort track - self.menu_sort_track_by = Menu(self.menu_trackconfiguration, tearoff=0) - self.menu_trackconfiguration.add_cascade(label=self.translate("Sort track"), menu=self.menu_sort_track_by) - for param in track_attr_possibilities: - self.menu_sort_track_by.add_radiobutton( - label=param.title(), - variable=self.stringvar_sort_track_by, - value=param - ) - - # select track - self.menu_trackselection = Menu(self.menu_trackconfiguration, tearoff=0) - self.menu_trackconfiguration.add_cascade(label=self.translate("Select track"), menu=self.menu_trackselection) - self.menu_trackselection_param = {} - - self.menu_trackhighlight = Menu(self.menu_trackconfiguration, tearoff=0) - self.menu_trackconfiguration.add_cascade(label=self.translate("Highlight track"), menu=self.menu_trackhighlight) - self.menu_trackhighlight_param = {} - - for param, values in track_attr_possibilities.items(): - for menu_param, menu in [ - (self.menu_trackselection_param, self.menu_trackselection), - (self.menu_trackhighlight_param, self.menu_trackhighlight) - ]: - menu_param[param] = { - "Menu": Menu(menu, tearoff=0), - "Var": [] - } - menu.add_cascade( - label=param.title(), - menu=menu_param[param]["Menu"] - ) - - for value in values: - menu_param[param]["Var"].append(BooleanVar(value=True)) - menu_param[param]["Menu"].add_checkbutton( - label=value, - variable=menu_param[param]["Var"][-1], - ) - # ADVANCED MENU ## INSTALLER PARAMETER self.menu_advanced = Menu(self.menu_bar, tearoff=0) @@ -286,7 +236,8 @@ class Gui: ## GAME PARAMETER self.menu_advanced.add_separator() - self.menu_advanced.add_checkbutton(label=self.translate("Use debug mode"), variable=self.boolvar_use_debug_mode) + self.menu_advanced.add_checkbutton(label=self.translate("Use debug mode"), + variable=self.boolvar_use_debug_mode) self.menu_mystuff = Menu(self.menu_advanced, tearoff=0) self.menu_advanced.add_cascade(label=self.translate("MyStuff"), menu=self.menu_mystuff) @@ -303,7 +254,7 @@ class Gui: self.menu_mystuff.entryconfig(index, label=self.translate( "Apply", " ", label, f" ({stringvar.get()!r} ", "selected", ")") - ) + ) _func(init=True) self.menu_mystuff.entryconfig(index, command=_func) @@ -318,6 +269,11 @@ class Gui: self.menu_help.add_command(label="Github Wiki", command=lambda: webbrowser.open(GITHUB_HELP_PAGE_URL)) self.menu_help.add_command(label="Discord", command=lambda: webbrowser.open(DISCORD_URL)) + def reload_ctconfig(self) -> None: + self.game.ctconfig.load_ctconfig_file( + ctconfig_file=self.get_ctconfig_path_pack(self.stringvar_ctconfig.get()) + ) + def get_available_packs(self) -> list: available_packs = [] @@ -335,13 +291,13 @@ class Gui: Check if an update is available """ try: - github_version_data = requests.get(VERSION_FILE_URL, allow_redirects=True).json() + github_version_data = requests.get(VERSION_FILE_URL, allow_redirects=True, timeout=3).json() with open("./version", "rb") as f: local_version_data = json.load(f) local_version = StrictVersion(f"{local_version_data['version']}.{local_version_data['subversion']}") github_version = StrictVersion(f"{github_version_data['version']}.{github_version_data['subversion']}") - if github_version > local_version: # if github version is newer than local version + if github_version > local_version: # if github version is newer than local version if messagebox.askyesno( self.translate("Update available !"), self.translate("An update is available, do you want to install it ?", @@ -360,8 +316,9 @@ class Gui: print(self.translate("finished extracting")) os.remove("./download.zip") - print(self.translate("starting application...")) - os.startfile(os.path.realpath("./Updater/Updater.exe")) + + print(self.translate("starting application...")) + os.startfile(os.path.realpath("./Updater/Updater.exe")) elif local_version > github_version: self.is_dev_version = True @@ -380,13 +337,18 @@ class Gui: """ error = traceback.format_exc() with open("./error.log", "a") as f: - f.write(f"---\n" - f"For game version : {self.game.ctconfig.version}\n" - f"./file/ directory : {os.listdir('./file/')}\n" - f"GAME/files/ information : {self.game.path, self.game.region}\n" - f"{error}\n" + f.write( + f"---\n" + f"For game version : {self.game.ctconfig.version}\n" + f"./file/ directory : {os.listdir('./file/')}\n" + f"ctconfig directory : {os.listdir(self.game.ctconfig.pack_path)}\n" + f"GAME/files/ information : {self.game.path, self.game.region}\n" + f"{error}\n" ) - messagebox.showerror(self.translate("Error"), self.translate("An error occured", " :", "\n", error, "\n\n")) + messagebox.showerror( + self.translate("Error"), + self.translate("An error occured", " :", "\n", error, "\n\n") + ) def progress(self, show: bool = None, indeter: bool = None, step: int = None, statut: str = None, max: int = None, add: int = None) -> None: diff --git a/source/Option.py b/source/Option.py index 6f2761f..7d3db7b 100644 --- a/source/Option.py +++ b/source/Option.py @@ -30,7 +30,7 @@ class Option: self.save_to_file() if need_restart: restart() - def load_from_file(self, option_file: str = "./option.json") -> None: + def load_from_file(self, option_file: str = "./option.json"): """ Load all options from a json file :param option_file: the file where to load option @@ -40,6 +40,8 @@ class Option: file_json = json.load(file) self.load_from_json(file_json) + return self + def load_from_json(self, option_json: dict) -> None: """ Load all options from a dictionnary diff --git a/source/Track.py b/source/Track.py index 509e93d..cab4a3c 100644 --- a/source/Track.py +++ b/source/Track.py @@ -5,8 +5,8 @@ from source.wszst import * HiddenTrackAttr = [ "file_wu8", "file_szs", - "track_wu8_dir", - "track_szs_dir" + "_track_wu8_dir", + "_track_szs_dir" ] # These attribute shouldn't be used to reference all the possibilities of values @@ -48,7 +48,6 @@ class Track: :param since_version: since when version did the track got added to the mod :param score: what it the score of the track :param warning: what is the warn level of the track (0 = none, 1 = minor bug, 2 = major bug) - :param note: note about the track :param track_wu8_dir: where is stored the track wu8 :param track_szs_dir: where is stored the track szs :param track_version: version of the track @@ -66,12 +65,11 @@ class Track: self.since_version = since_version # Since which version is this track available self.score = score # Track score between 1 and 3 stars self.warning = warning # Track bug level (1 = minor, 2 = major) - self.note = note # Note about the track self.version = version self.tags = tags - self.track_wu8_dir = track_wu8_dir - self.track_szs_dir = track_szs_dir + self._track_wu8_dir = track_wu8_dir + self._track_szs_dir = track_szs_dir self.file_wu8 = f"{track_wu8_dir}/{self.sha1}.wu8" self.file_szs = f"{track_szs_dir}/{self.sha1}.szs" @@ -100,7 +98,10 @@ class Track: """ convert track to szs """ - szs.normalize(src_file=self.file_wu8) + szs.normalize( + src_file=self.file_wu8, + dest_dir="./file/Track/" + ) def get_author_str(self) -> str: """ @@ -128,7 +129,8 @@ class Track: else: ctfile_text += ( f'"-"; ' # track path, not used in Race_*.szs, save a bit of space - f'"{self.get_track_formatted_name(ct_config, *args, **kwargs)}\\n{self.get_author_str()}"; ' # only in race show author's name + f'"{self.get_track_formatted_name(ct_config, *args, **kwargs)}\\n{self.get_author_str()}"; ' + # only in race show author's name f'"-"\n' # sha1, not used in Race_*.szs, save a bit of space ) @@ -185,8 +187,8 @@ class Track: for key, value in track_json.items(): # load all value in the json as class attribute setattr(self, key, value) - self.file_wu8 = f"{self.track_wu8_dir}/{self.sha1}.wu8" - self.file_szs = f"{self.track_szs_dir}/{self.sha1}.szs" + self.file_wu8 = f"{self._track_wu8_dir}/{self.sha1}.wu8" + self.file_szs = f"{self._track_szs_dir}/{self.sha1}.szs" return self diff --git a/source/wszst/img.py b/source/wszst/img.py index 4e88617..e91a3b3 100644 --- a/source/wszst/img.py +++ b/source/wszst/img.py @@ -5,11 +5,13 @@ WIMGT_PATH = "./tools/szs/wimgt" @error.better_wszst_error(wszst_tools=WIMGT_PATH) -def encode(file: str, format: str) -> None: +def encode(file: str, format: str, dest_file: str = None) -> None: """ Encode an .png image into a new format + :param dest_file: destination :param file: .png image :param format: new image format """ - subprocess.run([WIMGT_PATH, "ENCODE", file, "-x", format, "--overwrite"], - creationflags=subprocess.CREATE_NO_WINDOW, check=True, stdout=subprocess.PIPE) + cmd = [WIMGT_PATH, "ENCODE", file, "-x", format, "--overwrite"] + if dest_file: cmd.extend(["--dest", dest_file]) + subprocess.run(cmd, creationflags=subprocess.CREATE_NO_WINDOW, check=True, stdout=subprocess.PIPE)