diff --git a/source/Game.py b/source/Game.py index 5f48c0a..0293422 100644 --- a/source/Game.py +++ b/source/Game.py @@ -1,4 +1,4 @@ -from tkinter import messagebox +from tkinter import messagebox, StringVar, BooleanVar, IntVar from PIL import Image import shutil import glob @@ -40,8 +40,32 @@ class CantConvertTrack(Exception): super().__init__("Can't convert track, check if download are enabled.") +class NoGui: + """ + 'fake' gui if no gui are used for compatibility. + """ + def progression(self, *args, **kwargs): print(args, kwargs) + def translate(self, *args, **kwargs): return "" + def log_error(self, *args, **kwargs): print(args, kwargs) + + is_dev_version = False + + stringvar_game_format = StringVar() + boolvar_disable_download = BooleanVar() + intvar_process_track = IntVar() + boolvar_dont_check_track_sha1 = BooleanVar() + boolvar_del_track_after_conv = BooleanVar() + + class Game: def __init__(self, path: str = "", region_ID: str = "P", game_ID: str = "RMCP01", gui=None): + """ + Class about the game code and its treatment. + :param path: path of the game file / directory + :param region_ID: game's region id (P for PAL, K for KOR, ...) + :param game_ID: game's id (RMCP01 for PAL, ...) + :param gui: gui class used by the program + """ if not os.path.exists(path) and path: raise InvalidGamePath() self.extension = None self.path = path @@ -49,17 +73,21 @@ class Game: self.region = region_id_to_name[region_ID] self.region_ID = region_ID self.game_ID = game_ID - self.gui = gui + self.gui = gui if gui else NoGui self.ctconfig = CT_Config(gui=gui) - def set_path(self, path): + def set_path(self, path: str) -> None: + """ + Change game path + :param path: game's file + """ self.extension = get_extension(path).upper() self.path = path - def convert_to(self, format: str = "FST"): + def convert_to(self, format: str = "FST") -> None: """ + Convert game to an another format :param format: game format (ISO, WBFS, ...) - :return: converted game path """ if format in ["ISO", "WBFS", "CISO"]: path_game_format: str = os.path.realpath(self.path + "/../MKWFaraphel." + format.lower()) @@ -70,7 +98,10 @@ class Game: self.gui.progress(statut=self.gui.translate("Changing game's ID"), add=1) wszst.edit(self.path, region_ID=self.region_ID, name=f"Mario Kart Wii Faraphel {self.ctconfig.version}") - def extract(self): + def extract(self) -> None: + """ + Extract game file in the same directory. + """ if self.extension == "DOL": self.path = os.path.realpath(self.path + "/../../") # main.dol is in PATH/sys/, so go back 2 dir upper @@ -104,6 +135,9 @@ class Game: @in_thread def install_mod(self): + """ + Patch the game to install the mod + """ try: with open("./fs.json") as f: fs = json.load(f) @@ -204,14 +238,22 @@ class Game: finally: self.gui.progress(show=False) - def patch_autoadd(self, auto_add_dir: str = "./file/auto-add"): + def patch_autoadd(self, auto_add_dir: str = "./file/auto-add") -> None: + """ + Create the autoadd directory used to convert wbz track into szs + :param auto_add_dir: autoadd directory + """ if os.path.exists(auto_add_dir): shutil.rmtree(auto_add_dir) if not os.path.exists(self.path + "/tmp/"): os.makedirs(self.path + "/tmp/") wszst.autoadd(self.path, get_nodir(self.path) + "/tmp/auto-add/") shutil.move(self.path + "/tmp/auto-add/", auto_add_dir) shutil.rmtree(self.path + "/tmp/") - def patch_bmg(self, gamefile: str): # gamefile est le fichier .szs trouvé dans le /files/Scene/UI/ du jeu + def patch_bmg(self, gamefile: str) -> None: + """ + Patch bmg file (text file) + :param gamefile: an .szs file where file will be patched + """ NINTENDO_CWF_REPLACE = "Wiimmfi" MAINMENU_REPLACE = f"MKWFaraphel {self.ctconfig.version}" menu_replacement = { @@ -297,6 +339,9 @@ class Game: @in_thread 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("./convert_file.json") as f: @@ -329,13 +374,24 @@ class Game: finally: self.gui.progress(show=False) - def patch_image(self, fc): + def patch_image(self, fc) -> None: + """ + Convert .png image into the format wrote in convert_file + :param fc: + :return: + """ for i, file in enumerate(fc["img"]): self.gui.progress(statut=self.gui.translate("Converting images") + f"\n({i + 1}/{len(fc['img'])}) {file}", add=1) wszst.img_encode("./file/" + file, fc["img"][file]) - def patch_img_desc(self, img_desc_path: str = "./file/img_desc", dest_dir: str = "./file"): + def patch_img_desc(self, img_desc_path: str = "./file/img_desc", dest_dir: str = "./file") -> None: + """ + patch descriptive image used when the game boot + :param img_desc_path: directory where original part of the image are stored + :param dest_dir: directory where patched image will be saved + :return: + """ il = Image.open(img_desc_path + "/illustration.png") il_16_9 = il.resize((832, 456)) il_4_3 = il.resize((608, 456)) @@ -355,12 +411,21 @@ class Game: new_4_3.paste(img_lang_4_3, (0, 0), img_lang_4_3) new_4_3.save(dest_dir + f"/strapA_608x456{get_filename(get_nodir(file_lang))}.png") - def patch_tracks(self): + def patch_tracks(self) -> int: + """ + Download track's wu8 file and convert them to szs + :return: 0 if no error occured + """ max_process = self.gui.intvar_process_track.get() thread_list = {} error_count, error_max = 0, 3 - def add_process(track): + def add_process(track) -> int: + """ + a "single thread" to download, check sha1 and convert a track + :param track: the track that will be patched + :return: 0 if no error occured + """ nonlocal error_count, error_max, thread_list for _track in [track.file_szs, track.file_wu8]: @@ -370,21 +435,21 @@ class Game: if not self.gui.boolvar_disable_download.get(): while True: - download_returncode = track.download_wu8( - GITHUB_DEV_BRANCH if self.gui.is_dev_version else GITHUB_MASTER_BRANCH) - if download_returncode == -1: # can't download - error_count += 1 - if error_count > error_max: # Too much track wasn't correctly converted - messagebox.showerror( - self.gui.translate("Error"), - self.gui.translate("Too much tracks had a download issue.")) - raise TooMuchDownloadFailed() - else: - messagebox.showwarning(self.gui.translate("Warning"), - self.gui.translate("Can't download this track !", - f" ({error_count} / {error_max})")) - elif download_returncode == 2: - break # if download is disabled, do not check sha1 + download_returncode = 0 + if not os.path.exists(track.file_wu8): + download_returncode = track.download_wu8( + GITHUB_DEV_BRANCH if self.gui.is_dev_version else GITHUB_MASTER_BRANCH) + if download_returncode == -1: # can't download + error_count += 1 + if error_count > error_max: # Too much track wasn't correctly converted + messagebox.showerror( + self.gui.translate("Error"), + self.gui.translate("Too much tracks had a download issue.")) + raise TooMuchDownloadFailed() + else: + messagebox.showwarning(self.gui.translate("Warning"), + self.gui.translate("Can't download this track !", + f" ({error_count} / {error_max})")) if track.sha1: if not self.gui.boolvar_dont_check_track_sha1.get(): @@ -399,7 +464,7 @@ class Game: break - if not (os.path.exists(track.file_szs)) or download_returncode == 3: + if not os.path.exists(track.file_szs) or download_returncode == 3: # returncode 3 is track has been updated if os.path.exists(track.file_wu8): track.convert_wu8_to_szs() @@ -411,29 +476,17 @@ class Game: os.remove(track.file_wu8) return 0 - def clean_process(): + def clean_process() -> int: + """ + Check if a track conversion ended, and remove them from thread_list + :return: 0 if thread_list is empty, else 1 + """ nonlocal error_count, error_max, thread_list for track_key, thread in thread_list.copy().items(): if not thread.is_alive(): # if conversion ended thread_list.pop(track_key) - """stderr = thread.stderr.read() - if b"wszst: ERROR" in stderr: # Error occured - os.remove(track.file_szs) - error_count += 1 - if error_count > error_max: # Too much track wasn't correctly converted - messagebox.showerror( - self.gui.translate("Error"), - self.gui.translate("Too much track had a conversion issue.")) - raise CantConvertTrack() - else: # if the error max hasn't been reach - messagebox.showwarning( - self.gui.translate("Warning"), - self.gui.translate("The track", " ", track.file_wu8, - "do not have been properly converted.", - f" ({error_count} / {error_max})")) - else: - if self.gui.boolvar_del_track_after_conv.get(): os.remove(track.file_wu8)""" + if self.gui.boolvar_del_track_after_conv.get(): os.remove(track.file_wu8) if not (any(thread_list.values())): return 1 # if there is no more process if len(thread_list): return 1 @@ -441,7 +494,7 @@ class Game: total_track = len(self.ctconfig.all_tracks) for i, track in enumerate(self.ctconfig.all_tracks): - while True: + while error_count <= error_max: if len(thread_list) < max_process: track_name = track.get_track_name() thread_list[track_name] = Thread(target=add_process, args=[track]) @@ -455,4 +508,4 @@ class Game: while clean_process() != 1: pass # End the process if all process ended - return 0 \ No newline at end of file + return 0 diff --git a/source/Gui.py b/source/Gui.py index 6109e31..659c680 100644 --- a/source/Gui.py +++ b/source/Gui.py @@ -20,6 +20,9 @@ def restart(): class Gui: def __init__(self): + """ + Initialize program Gui + """ self.root = Tk() self.option = Option() @@ -156,7 +159,10 @@ class Gui: self.progressbar = ttk.Progressbar(self.root) self.progresslabel = Label(self.root) - def check_update(self): + def check_update(self) -> None: + """ + Check if an update is available + """ try: gitversion = requests.get(VERSION_FILE_URL, allow_redirects=True).json() with open("./version", "rb") as f: @@ -200,13 +206,26 @@ class Gui: except: self.log_error() - def log_error(self): + def log_error(self) -> None: + """ + When an error occur, will show it in a messagebox and write it in error.log + """ error = traceback.format_exc() with open("./error.log", "a") as f: f.write(f"---\n{error}\n") messagebox.showerror(self.translate("Error"), self.translate("An error occured", " :", "\n", error, "\n\n")) - def progress(self, show=None, indeter=None, step=None, statut=None, max=None, add=None): + def progress(self, show: bool = None, indeter: bool = None, step: int = None, + statut: str = None, max: int = None, add: int = None) -> None: + """ + configure the progress bar shown when doing a task + :param show: show or hide the progress bar + :param indeter: if indeter, the progress bar will do a infinite loop animation + :param step: set the progress of the bar + :param statut: text shown under the progress bar + :param max: set the maximum step + :param add: add to step of the progress bar + """ if indeter is True: self.progressbar.config(mode="indeterminate") self.progressbar.start(50) @@ -229,7 +248,11 @@ class Gui: self.progressbar["value"] = 0 if add: self.progressbar.step(add) - def state_button(self, enable=True): + def state_button(self, enable: bool = True) -> None: + """ + used to enable or disable button when doing task + :param enable: are the button enabled ? + """ button = [ self.button_game_extract, self.button_install_mod, @@ -242,17 +265,18 @@ class Gui: else: widget.config(state=DISABLED) - def translate(self, *texts, lang=None): - if lang is None: - lang = self.stringvar_language.get() - elif lang == "F": - lang = "fr" - elif lang == "G": - lang = "ge" - elif lang == "I": - lang = "it" - elif lang == "S": - lang = "sp" + def translate(self, *texts, lang: str = None) -> str: + """ + translate text into an another language in translation.json file + :param texts: all text to convert + :param lang: force a destination language to convert track + :return: translated text + """ + if lang is None: lang = self.stringvar_language.get() + elif lang == "F": lang = "fr" + elif lang == "G": lang = "ge" + elif lang == "I": lang = "it" + elif lang == "S": lang = "sp" if lang in translation_dict: _lang_trad = translation_dict[lang] diff --git a/source/Track.py b/source/Track.py index f094a2f..ef0fa38 100644 --- a/source/Track.py +++ b/source/Track.py @@ -6,10 +6,31 @@ from . import wszst class Track: - def __init__(self, name: str = "_", file_wu8: str = None, file_szs: str = None, prefix: str = None, suffix: str = None, + def __init__(self, name: str = "_", prefix: str = None, suffix: str = None, author="Nintendo", special="T11", music="T11", new=True, sha1: str = None, since_version: str = None, score: int = 0, warning: int = 0, note: str = "", track_wu8_dir: str = "./file/Track-WU8/", track_szs_dir: str = "./file/Track/", *args, **kwargs): + """ + Track class + :param name: track name + :param file_wu8: path to its wu8 file + :param file_szs: path to its szs file + :param prefix: track prefix (often original console or game) + :param suffix: track suffix (often for variation like Boost or Night) + :param author: track creator + :param special: track special slot + :param music: track music slot + :param new: is the track original or from an older game + :param sha1: track sha1 + :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 args: / + :param kwargs: / + """ self.name = name # Track name self.prefix = prefix # Prefix, often used for game or original console like Wii U, DS, ... @@ -28,18 +49,35 @@ class Track: self.file_wu8 = f"{track_wu8_dir}/{self.get_track_name()}.wu8" self.file_szs = f"{track_szs_dir}/{self.get_track_name()}.szs" - def __repr__(self): + def __repr__(self) -> str: + """ + track representation when printed + :return: track information + """ return f"{self.get_track_name()} sha1={self.sha1} score={self.score}" - def check_sha1(self): + def check_sha1(self) -> int: + """ + check if track wu8's sha1 is correct + :return: 0 if yes, -1 if no + """ ws = wszst.sha1(self.file_wu8) if wszst.sha1(self.file_wu8) == self.sha1: return 0 else: return -1 - def convert_wu8_to_szs(self): + def convert_wu8_to_szs(self) -> str: + """ + convert track to szs + :return: path to szs track + """ return wszst.normalize(src_file=self.file_wu8) - def download_wu8(self, github_content_root: str): + def download_wu8(self, github_content_root: str) -> int: + """ + download track wu8 from github + :param github_content_root: url to github project root + :return: 0 if correctly downloaded, 1 if no need to download, 3 if track size is incorrect, -1 if error + """ returncode = 0 dl = requests.get(github_content_root + self.file_wu8, allow_redirects=True, stream=True) @@ -60,8 +98,9 @@ class Track: print(f"error {dl.status_code} {self.file_wu8}") return -1 - def get_ctfile(self, race=False, *args, **kwargs): + def get_ctfile(self, race=False, *args, **kwargs) -> str: """ + get ctfile text to create CTFILE.txt and RCTFILE.txt :param race: is it a text used for Race_*.szs ? :return: ctfile definition for the track """ @@ -84,8 +123,9 @@ class Track: return ctfile_text - def get_track_formatted_name(self, highlight_version: str = None): + def get_track_formatted_name(self, highlight_version: str = None) -> str: """ + get the track name with score, color, ... :param highlight_version: if a specific version need to be highlighted. :return: the name of the track with colored prefix, suffix """ @@ -112,14 +152,22 @@ class Track: name = name.replace("_", " ") return name - def get_track_name(self): + def get_track_name(self) -> str: + """ + get the track name without score, color... + :return: track name + """ prefix = (self.prefix + " ") if self.prefix else "" suffix = (" (" + self.suffix + ")") if self.suffix else "" name = (prefix + self.name + suffix) return name - def load_from_json(self, track_json: dict): + def load_from_json(self, track_json: dict) -> None: + """ + load the track from a dictionary + :param track_json: track's dictionnary + """ for key, value in track_json.items(): # load all value in the json as class attribute setattr(self, key, value)