From b3d5af63ed4ca447c5219cbc5997aca67a522608 Mon Sep 17 00:00:00 2001 From: Faraphel Date: Tue, 25 Jan 2022 12:59:51 +0100 Subject: [PATCH] readded a better track selection filter (advanced menu), fixed an issue with tracks always renormalizing, added a default sort in ct_config, added a Common class to reunite all component with more readability and less weird code, splited the Gui into a directory --- Pack/MKWFaraphel/ct_config.json | 1 + main.pyw | 6 +- source/CT_Config.py | 26 ++- source/Common.py | 21 +++ source/Game.py | 175 ++++++++------------ source/Gui/CheatManager.py | 0 source/{Gui.py => Gui/Main.py} | 76 +++++---- source/Gui/SelectPack.py | 3 + source/Gui/TrackSelection.py | 283 ++++++++++++++++++++++++++++++++ source/Track.py | 49 +++--- 10 files changed, 463 insertions(+), 177 deletions(-) create mode 100644 source/Common.py create mode 100644 source/Gui/CheatManager.py rename source/{Gui.py => Gui/Main.py} (87%) create mode 100644 source/Gui/SelectPack.py create mode 100644 source/Gui/TrackSelection.py diff --git a/Pack/MKWFaraphel/ct_config.json b/Pack/MKWFaraphel/ct_config.json index fd1cfab..b518e31 100644 --- a/Pack/MKWFaraphel/ct_config.json +++ b/Pack/MKWFaraphel/ct_config.json @@ -1804,6 +1804,7 @@ ] } ], + "default_sort": "name", "tracks_list":[ { "name":"4IT Clown's Road", diff --git a/main.pyw b/main.pyw index ad1b0b3..a7d2be6 100644 --- a/main.pyw +++ b/main.pyw @@ -1,4 +1,4 @@ -from source.Gui import Gui +from source.Common import Common -gui = Gui() -gui.root.mainloop() \ No newline at end of file +common = Common() +common.mainloop() diff --git a/source/CT_Config.py b/source/CT_Config.py index f182058..c9c8a80 100644 --- a/source/CT_Config.py +++ b/source/CT_Config.py @@ -9,10 +9,10 @@ from source.Track import Track, get_trackdata_from_json 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, + game_variant: str = "01", region: int = None, cheat_region: int = None, tags_color: dict = None, prefix_list: list = None, suffix_list: list = None, tag_retro: str = "Retro", default_track: Track = None, pack_path: str = "", - file_process: dict = None, file_structure: dict = None): + file_process: dict = None, file_structure: dict = None, default_sort: str = "name"): self.version = version self.name = name @@ -23,7 +23,6 @@ class CT_Config: self.ordered_cups = [] self.unordered_tracks = [] - self.gui = gui self.tags_color = tags_color if tags_color else {} self.prefix_list = prefix_list if tags_color else [] @@ -32,10 +31,15 @@ class CT_Config: self.default_track = default_track self.pack_path = pack_path + self.sort_track_attr = default_sort self.file_process = file_process self.file_structure = file_structure + self.filter_track_selection = lambda track: True + self.filter_track_highlight = lambda track: False + self.filter_track_random_new = lambda track: getattr(track, "new", False) + def add_ordered_cup(self, cup: Cup) -> None: """ add a cup to the config @@ -53,9 +57,11 @@ class CT_Config: def unordered_tracks_to_cup(self): track_in_cup: int = 4 - for cup_id, track_id in enumerate(range(0, len(self.unordered_tracks), track_in_cup), start=1): + track_selection = list(filter(self.filter_track_selection, self.unordered_tracks)) + + for cup_id, track_id in enumerate(range(0, len(track_selection), track_in_cup), start=1): cup = Cup(id=cup_id, name=f"CT{cup_id}") - for index, track in enumerate(self.unordered_tracks[track_id:track_id + track_in_cup]): + for index, track in enumerate(track_selection[track_id:track_id + track_in_cup]): cup.tracks[index] = track yield cup @@ -77,8 +83,13 @@ class CT_Config: ctfile.write(header); rctfile.write(header) # all cups + kwargs = { + "filter_highlight": self.filter_track_highlight, + "filter_random_new": self.filter_track_random_new, + "ct_config": self + } + for cup in self.get_all_cups(): - kwargs = {"highlight_version": highlight_version, "ct_config": self} ctfile.write(cup.get_ctfile(race=False, **kwargs)) rctfile.write(cup.get_ctfile(race=True, **kwargs)) @@ -183,8 +194,9 @@ class CT_Config: self.version = ctconfig_json.get("version") if "name" in ctconfig_json: self.name = ctconfig_json["name"] - 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"] + if "default_sort" in ctconfig_json: self.default_sort = ctconfig_json["default_sort"] + self.nickname = ctconfig_json["nickname"] if "nickname" in ctconfig_json else self.name for param in ["region", "cheat_region", "tags_color", "prefix_list", "suffix_list", "tag_retro"]: setattr(self, param, ctconfig_json.get(param)) diff --git a/source/Common.py b/source/Common.py new file mode 100644 index 0000000..1d0f570 --- /dev/null +++ b/source/Common.py @@ -0,0 +1,21 @@ +from source.CT_Config import CT_Config +from source.Option import Option +from source.Game import Game +from source.Gui.Main import Main +from source.Gui.TrackSelection import TrackSelection + +class Common: + def __init__(self): + """ + Common allow to store multiple object that need each other and still make the code readable enough without + having to access an object with some obscure way + """ + + self.option = Option().load_from_file("./option.json") + self.ct_config = CT_Config() + self.game = Game(common=self) + + self.gui_main = Main(common=self) + + def show_gui_track_configuration(self): TrackSelection(common=self) + def mainloop(self): self.gui_main.mainloop() \ No newline at end of file diff --git a/source/Game.py b/source/Game.py index 9220f14..faca991 100644 --- a/source/Game.py +++ b/source/Game.py @@ -4,53 +4,19 @@ import shutil import glob import json -from source.CT_Config import CT_Config from source.definition import * from source.wszst import * from source.Error import * -class NoGui: - """ - 'fake' gui if no gui are used for compatibility. - """ - - class NoButton: - def grid(self, *args, **kwargs): pass - - def config(self, *args, **kwargs): pass - - class NoVariable: - def __init__(self, value=None): - self.value = None - - def set(self, value): - self.value = value - - def get(self): - return self.value - - def progress(*args, **kwargs): print(args, kwargs) - - def translate(*args, **kwargs): return "" - - def log_error(*args, **kwargs): print(args, kwargs) - - is_dev_version = False - button_install_mod = NoButton() - stringvar_game_format = NoVariable() - intvar_process_track = NoVariable() - boolvar_dont_check_track_sha1 = NoVariable() - - class Game: - def __init__(self, path: str = "", region_ID: str = "P", game_ID: str = "RMCP01", gui=None): + def __init__(self, common, path: str = "", region_ID: str = "P", game_ID: str = "RMCP01"): """ 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 + :param common: common class to access all other element """ if not os.path.exists(path) and path: raise InvalidGamePath() self.extension = None @@ -59,8 +25,7 @@ class Game: self.region = region_id_to_name[region_ID] self.region_ID = region_ID self.game_ID = game_ID - self.gui = gui if gui else NoGui - self.ctconfig = CT_Config(gui=gui) + self.common = common def set_path(self, path: str) -> None: """ @@ -76,17 +41,17 @@ class Game: :param format: game format (ISO, WBFS, ...) """ if format in ["ISO", "WBFS", "CISO"]: - path_game_format: str = os.path.realpath(self.path + f"/../{self.ctconfig.nickname} v{self.ctconfig.version}." + format.lower()) + path_game_format: str = os.path.realpath(self.path + f"/../{self.common.ct_config.nickname} v{self.common.ct_config.version}." + format.lower()) wit.copy(src_path=self.path, dst_path=path_game_format, format=format) shutil.rmtree(self.path) self.path = path_game_format - self.gui.progress(statut=self.gui.translate("Changing game's ID"), add=1) + self.common.gui_main.progress(statut=self.common.gui_main.translate("Changing game's ID"), add=1) wit.edit( file=self.path, region_ID=self.region_ID, - game_variant=self.ctconfig.game_variant, - name=f"{self.ctconfig.name} {self.ctconfig.version}" + game_variant=self.common.ct_config.game_variant, + name=f"{self.common.ct_config.name} {self.common.ct_config.version}" ) def extract(self) -> None: @@ -100,7 +65,7 @@ class Game: # Fiding a directory name that doesn't already exist path_dir = get_next_available_dir( parent_dir=self.path + f"/../", - dir_name=f"{self.ctconfig.nickname} v{self.ctconfig.version}" + dir_name=f"{self.common.ct_config.nickname} v{self.common.ct_config.version}" ) wit.extract(file=self.path, dst_dir=path_dir) @@ -128,7 +93,7 @@ class Game: count all the step patching subfile will take (for the progress bar) :return: number of step estimated """ - with open(f"{self.ctconfig.pack_path}/file_structure.json") as f: + with open(f"{self.common.ct_config.pack_path}/file_structure.json") as f: fs = json.load(f) # This part is used to estimate the max_step @@ -160,11 +125,11 @@ class Game: """ patch subfile as indicated in the file_structure.json file (for file structure) """ - with open(f"{self.ctconfig.pack_path}/file_structure.json") as f: + with open(f"{self.common.ct_config.pack_path}/file_structure.json") as f: fs = json.load(f) extracted_file = [] - self.gui.progress(show=True, indeter=False, statut=self.gui.translate("Modifying subfile..."), add=1) + self.common.gui_main.progress(show=True, indeter=False, statut=self.common.gui_main.translate("Modifying subfile..."), add=1) def replace_file(path, file, subpath="/") -> None: """ @@ -173,10 +138,10 @@ class Game: :param file: file to replace :param subpath: directory between .szs file and file inside to replace """ - self.gui.progress(statut=self.gui.translate("Editing", "\n", get_nodir(path)), add=1) + self.common.gui_main.progress(statut=self.common.gui_main.translate("Editing", "\n", get_nodir(path)), add=1) extension = get_extension(path) - source_file = f"{self.ctconfig.pack_path}/file/{file}" + source_file = f"{self.common.ct_config.pack_path}/file/{file}" dest_file = path if extension == "szs": @@ -205,7 +170,7 @@ class Game: for ffp in fs[fp][nf]: replace_file(path=f, subpath=nf, file=ffp) for file in extracted_file: - self.gui.progress(statut=self.gui.translate("Recompilating", "\n", get_nodir(file)), add=1) + self.common.gui_main.progress(statut=self.common.gui_main.translate("Recompilating", "\n", get_nodir(file)), add=1) szs.create(file=file) shutil.rmtree(file + ".d", ignore_errors=True) @@ -213,9 +178,9 @@ class Game: """ copy MyStuff directory into the game *before* patching the game """ - self.gui.progress(show=True, indeter=False, statut=self.gui.translate("Copying MyStuff..."), add=1) + self.common.gui_main.progress(show=True, indeter=False, statut=self.common.gui_main.translate("Copying MyStuff..."), add=1) - mystuff_folder = self.gui.stringvar_mystuff_folder.get() + mystuff_folder = self.common.gui_main.stringvar_mystuff_folder.get() if mystuff_folder and mystuff_folder != "None": # replace game's file by files with the same name in the MyStuff root @@ -241,27 +206,27 @@ class Game: """ patch the main.dol file to allow the addition of LECODE.bin file """ - self.gui.progress(statut=self.gui.translate("Patch main.dol"), add=1) + self.common.gui_main.progress(statut=self.common.gui_main.translate("Patch main.dol"), add=1) - region_id = self.ctconfig.region if self.gui.is_using_official_config() else self.ctconfig.cheat_region + region_id = self.common.ct_config.region if self.common.gui_main.is_using_official_config() else self.common.ct_config.cheat_region wstrt.patch(path=self.path, region_id=region_id) def install_patch_lecode(self) -> None: """ configure and add the LECODE.bin file to the mod """ - self.gui.progress(statut=self.gui.translate("Patch lecode.bin"), add=1) + self.common.gui_main.progress(statut=self.common.gui_main.translate("Patch lecode.bin"), add=1) - lpar_path = self.ctconfig.file_process["placement"].get("lpar_dir") + lpar_path = self.common.ct_config.file_process["placement"].get("lpar_dir") if not lpar_path: f"" lpar_path = ( - f"{self.ctconfig.pack_path}/file/{lpar_path}/" - f"lpar-{'debug' if self.gui.boolvar_use_debug_mode.get() else 'normal'}.txt" + f"{self.common.ct_config.pack_path}/file/{lpar_path}/" + f"lpar-{'debug' if self.common.gui_main.boolvar_use_debug_mode.get() else 'normal'}.txt" ) - lecode_file = self.ctconfig.file_process["placement"].get("lecode_bin_dir") + lecode_file = self.common.ct_config.file_process["placement"].get("lecode_bin_dir") if not lecode_file: lecode_file = "" - lecode_file = f"{self.ctconfig.pack_path}/file/{lecode_file}/lecode-{self.region}.bin" + lecode_file = f"{self.common.ct_config.pack_path}/file/{lecode_file}/lecode-{self.region}.bin" lec.patch( lecode_file=lecode_file, @@ -277,8 +242,8 @@ class Game: """ convert the rom to the selected game format """ - output_format = self.gui.stringvar_game_format.get() - self.gui.progress(statut=self.gui.translate("Converting to", " ", output_format), add=1) + output_format = self.common.gui_main.stringvar_game_format.get() + self.common.gui_main.progress(statut=self.common.gui_main.translate("Converting to", " ", output_format), add=1) self.convert_to(output_format) def install_mod(self) -> None: @@ -289,22 +254,22 @@ class Game: max_step = 5 + self.count_patch_subfile_operation() # PATCH main.dol and PATCH lecode.bin, converting, changing ID, copying MyStuff Folder - self.gui.progress(statut=self.gui.translate("Installing mod..."), max=max_step, step=0) + self.common.gui_main.progress(statut=self.common.gui_main.translate("Installing mod..."), max=max_step, step=0) self.install_copy_mystuff() self.install_patch_subfile() self.install_patch_maindol() self.install_patch_lecode() self.install_convert_rom() - messagebox.showinfo(self.gui.translate("End"), self.gui.translate("The mod have been installed !")) + messagebox.showinfo(self.common.gui_main.translate("End"), self.common.gui_main.translate("The mod have been installed !")) except Exception as e: - self.gui.log_error() + self.common.gui_main.log_error() raise e finally: - self.gui.progress(show=False) - self.gui.quit() + self.common.gui_main.progress(show=False) + self.common.gui_main.quit() def patch_autoadd(self, auto_add_dir: str = "./file/auto-add") -> None: """ @@ -337,15 +302,15 @@ class Game: track_id = bmgtrack[start_track_id:start_track_id + 3] if track_id[1] in "1234": # if the track is a original track from the wii prefix = "Wii" - if prefix in self.ctconfig.tags_color: - prefix = "\\\\c{" + self.ctconfig.tags_color[prefix] + "}" + prefix + "\\\\c{off}" + if prefix in self.common.ct_config.tags_color: + prefix = "\\\\c{" + self.common.ct_config.tags_color[prefix] + "}" + prefix + "\\\\c{off}" prefix += " " elif track_id[1] in "5678": # if the track is a retro track from the original game prefix, *track_name = track_name.split(" ") track_name = " ".join(track_name) - if prefix in self.ctconfig.tags_color: - prefix = "\\\\c{" + self.ctconfig.tags_color[prefix] + "}" + prefix + "\\\\c{off}" + if prefix in self.common.ct_config.tags_color: + prefix = "\\\\c{" + self.common.ct_config.tags_color[prefix] + "}" + prefix + "\\\\c{off}" prefix += " " track_id = hex(bmgID_track_move[track_id])[2:] @@ -364,15 +329,15 @@ class Game: """ bmg_replacement = { - "MOD_NAME": self.ctconfig.name, - "MOD_NICKNAME": self.ctconfig.nickname, - "MOD_VERSION": self.ctconfig.version, - "MOD_CUSTOMIZED": "" if self.gui.is_using_official_config() else "(custom)", + "MOD_NAME": self.common.ct_config.name, + "MOD_NICKNAME": self.common.ct_config.nickname, + "MOD_VERSION": self.common.ct_config.version, + "MOD_CUSTOMIZED": "" if self.common.gui_main.is_using_official_config() else "(custom)", "ONLINE_SERVICE": "Wiimmfi", } bmglang = gamefile[-len("E.txt"):-len(".txt")] # Langue du fichier - self.gui.progress(statut=self.gui.translate("Patching text", " ", bmglang), add=1) + self.common.gui_main.progress(statut=self.common.gui_main.translate("Patching text", " ", bmglang), add=1) szs.extract(file=gamefile) @@ -401,7 +366,7 @@ class Game: :param bmg_language: language of the bmg file :return: the replaced bmg file """ - with open(f"{self.ctconfig.pack_path}/file_process.json", encoding="utf8") as fp_file: + with open(f"{self.common.ct_config.pack_path}/file_process.json", encoding="utf8") as fp_file: file_process = json.load(fp_file) for bmg_process in file_process["bmg"]: @@ -440,8 +405,8 @@ class Game: bmg.encode(file) os.remove(file) - bmg_dir = self.ctconfig.file_process["placement"].get("bmg_patch_dir") - bmg_dir = f"{self.ctconfig.pack_path}/file/{bmg_dir if bmg_dir else ''}" + bmg_dir = self.common.ct_config.file_process["placement"].get("bmg_patch_dir") + bmg_dir = f"{self.common.ct_config.pack_path}/file/{bmg_dir if bmg_dir else ''}" os.makedirs(get_dir(bmg_dir), exist_ok=True) save_bmg(f"{bmg_dir}/Menu_{bmglang}.txt", process_bmg_replacement(bmgmenu, bmglang)) @@ -457,18 +422,18 @@ class Game: Prepare all files to install the mod (track, bmg text, descriptive image, ...) """ try: - os.makedirs(f"{self.ctconfig.pack_path}/file/Track-WU8/", exist_ok=True) + os.makedirs(f"{self.common.ct_config.pack_path}/file/Track-WU8/", exist_ok=True) - max_step = len(self.ctconfig.file_process["img_encode"]) + \ - len(self.ctconfig.all_tracks) + \ + max_step = len(self.common.ct_config.file_process["img_encode"]) + \ + len(self.common.ct_config.all_tracks) + \ 3 + \ len("EGFIS") - self.gui.progress(show=True, indeter=False, statut=self.gui.translate("Converting files"), + self.common.gui_main.progress(show=True, indeter=False, statut=self.common.gui_main.translate("Converting files"), max=max_step, step=0) - self.gui.progress(statut=self.gui.translate("Configurating LE-CODE"), add=1) - self.ctconfig.create_ctfile( - highlight_version=self.gui.stringvar_mark_track_from_version.get(), + self.common.gui_main.progress(statut=self.common.gui_main.translate("Configurating LE-CODE"), add=1) + self.common.ct_config.create_ctfile( + highlight_version=self.common.gui_main.stringvar_mark_track_from_version.get(), ) self.generate_cticons() @@ -479,37 +444,37 @@ class Game: self.patch_tracks() except Exception as e: - self.gui.log_error() + self.common.gui_main.log_error() raise e finally: - self.gui.progress(show=False) + self.common.gui_main.progress(show=False) def generate_cticons(self): - file = self.ctconfig.file_process["placement"].get("ct_icons") + file = self.common.ct_config.file_process["placement"].get("ct_icons") if not file: file = "ct_icons.tpl.png" - file = f"{self.ctconfig.pack_path}/file/{file}" + file = f"{self.common.ct_config.pack_path}/file/{file}" os.makedirs(get_dir(file), exist_ok=True) - self.ctconfig.get_cticon().save(file) + self.common.ct_config.get_cticon().save(file) def patch_image(self) -> None: """ Convert .png image into the format wrote in convert_file """ - image_amount = len(self.ctconfig.file_process["img_encode"]) + image_amount = len(self.common.ct_config.file_process["img_encode"]) - for i, (file, data) in enumerate(self.ctconfig.file_process["img_encode"].items()): - self.gui.progress( - statut=self.gui.translate("Converting images") + f"\n({i + 1}/{image_amount}) {file}", + for i, (file, data) in enumerate(self.common.ct_config.file_process["img_encode"].items()): + self.common.gui_main.progress( + statut=self.common.gui_main.translate("Converting images") + f"\n({i + 1}/{image_amount}) {file}", add=1 ) img.encode( - file=f"{self.ctconfig.pack_path}/file/{file}", + file=f"{self.common.ct_config.pack_path}/file/{file}", format=data["format"], - dest_file=f"{self.ctconfig.pack_path}/file/{data['dest']}" if "dest" in data else None + dest_file=f"{self.common.ct_config.pack_path}/file/{data['dest']}" if "dest" in data else None ) def generate_image(self, generator: dict) -> Image.Image: @@ -546,7 +511,7 @@ class Game: tuple(layer["color"]) if "color" in layer else 0 ) if layer["type"] == "image": - layer_image = Image.open(f'{self.ctconfig.pack_path}/file/{layer["path"]}') + layer_image = Image.open(f'{self.common.ct_config.pack_path}/file/{layer["path"]}') layer_image = layer_image.resize(get_layer_size(layer)).convert("RGBA") image.paste( layer_image, @@ -555,7 +520,7 @@ class Game: ) if layer["type"] == "text": font = ImageFont.truetype( - font=f'{self.ctconfig.pack_path}/file/{layer["font"]}' if "font" in layer else None, + font=f'{self.common.ct_config.pack_path}/file/{layer["font"]}' if "font" in layer else None, size=int(layer["text_size"] * generator["height"]) if "text_size" in layer else 10, ) draw.text( @@ -568,8 +533,8 @@ class Game: return image def generate_all_image(self) -> None: - for file, generator in self.ctconfig.file_process["img_generator"].items(): - file = f"{self.ctconfig.pack_path}/file/{file}" + for file, generator in self.common.ct_config.file_process["img_generator"].items(): + file = f"{self.common.ct_config.pack_path}/file/{file}" os.makedirs(get_dir(file), exist_ok=True) self.generate_image(generator).save(file) @@ -577,7 +542,7 @@ class Game: """ Download track's wu8 file and convert them to szs """ - max_process = self.gui.intvar_process_track.get() + max_process = self.common.gui_main.intvar_process_track.get() thread_list = {} error_count, error_max = 0, 3 @@ -607,16 +572,16 @@ class Game: return bool(thread_list) - total_track = self.ctconfig.get_tracks_count() - self.gui.progress(max=total_track, indeter=False, show=True) + total_track = self.common.ct_config.get_tracks_count() + self.common.gui_main.progress(max=total_track, indeter=False, show=True) - for i, track in enumerate(self.ctconfig.get_tracks()): + for i, track in enumerate(self.common.ct_config.get_tracks()): while error_count <= error_max: if len(thread_list) < max_process: thread_list[track.sha1] = Thread(target=add_process, args=[track]) thread_list[track.sha1].setDaemon(True) thread_list[track.sha1].start() - self.gui.progress(statut=self.gui.translate("Converting tracks", f"\n({i + 1}/{total_track})\n", + self.common.gui_main.progress(statut=self.common.gui_main.translate("Converting tracks", f"\n({i + 1}/{total_track})\n", "\n".join(thread_list.keys())), add=1) break clean_process() diff --git a/source/Gui/CheatManager.py b/source/Gui/CheatManager.py new file mode 100644 index 0000000..e69de29 diff --git a/source/Gui.py b/source/Gui/Main.py similarity index 87% rename from source/Gui.py rename to source/Gui/Main.py index 757ce8c..5543763 100644 --- a/source/Gui.py +++ b/source/Gui/Main.py @@ -8,9 +8,7 @@ import zipfile import glob import json -from source.Game import Game, RomAlreadyPatched, InvalidGamePath, InvalidFormat -from source.Option import Option - +from source.Error import * from source.definition import * @@ -18,8 +16,8 @@ with open("./translation.json", encoding="utf-8") as f: translation_dict = json.load(f) -class Gui: - def __init__(self) -> None: +class Main: + def __init__(self, common) -> None: """ Initialize program Gui """ @@ -27,9 +25,7 @@ class Gui: self.root.resizable(False, False) self.root.iconbitmap(bitmap="./icon.ico") - self.option = Option().load_from_file("./option.json") - self.game = Game(gui=self) - + self.common = common self.menu_bar = None self.available_packs = self.get_available_packs() @@ -42,10 +38,10 @@ class Gui: self.is_dev_version = False # Is this installer version a dev ? self.stringvar_ctconfig = StringVar(value=self.available_packs[0]) - self.stringvar_language = StringVar(value=self.option.language) - self.stringvar_game_format = StringVar(value=self.option.format) - 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.stringvar_language = StringVar(value=self.common.option.language) + self.stringvar_game_format = StringVar(value=self.common.option.format) + self.boolvar_dont_check_for_update = BooleanVar(value=self.common.option.dont_check_for_update) + self.intvar_process_track = IntVar(value=self.common.option.process_track) self.root.title(self.translate("MKWFaraphel Installer")) @@ -104,9 +100,9 @@ class Gui: game_path = entry_game_path.get() if not os.path.exists(game_path): raise InvalidGamePath - self.game.set_path(game_path) + self.common.game.set_path(game_path) self.progress(show=True, indeter=True, statut=self.translate("Extracting the game...")) - self.game.extract() + self.common.game.extract() except RomAlreadyPatched: messagebox.showerror(self.translate("Error"), self.translate("This game is already modded")) @@ -126,8 +122,8 @@ class Gui: @in_thread def do_everything(): use_path() - self.game.patch_file() - self.game.install_mod() + self.common.game.patch_file() + self.common.game.install_mod() self.button_do_everything = Button( self.frame_game_path_action, @@ -152,13 +148,13 @@ class Gui: label="Français", variable=self.stringvar_language, value="fr", - command=lambda: self.option.edit("language", "fr", need_restart=True) + command=lambda: self.common.option.edit("language", "fr", need_restart=True) ) self.menu_language.add_radiobutton( label="English", variable=self.stringvar_language, value="en", - command=lambda: self.option.edit("language", "en", need_restart=True) + command=lambda: self.common.option.edit("language", "en", need_restart=True) ) # OUTPUT FORMAT MENU @@ -168,25 +164,25 @@ class Gui: label=self.translate("FST (Directory)"), variable=self.stringvar_game_format, value="FST", command=lambda: - self.option.edit("format", "FST") + self.common.option.edit("format", "FST") ) self.menu_format.add_radiobutton( label="ISO", variable=self.stringvar_game_format, value="ISO", - command=lambda: self.option.edit("format", "ISO") + command=lambda: self.common.option.edit("format", "ISO") ) self.menu_format.add_radiobutton( label="CISO", variable=self.stringvar_game_format, value="CISO", - command=lambda: self.option.edit("format", "CISO") + command=lambda: self.common.option.edit("format", "CISO") ) self.menu_format.add_radiobutton( label="WBFS", variable=self.stringvar_game_format, value="WBFS", - command=lambda: self.option.edit("format", "WBFS") + command=lambda: self.common.option.edit("format", "WBFS") ) # ADVANCED MENU @@ -196,7 +192,7 @@ class Gui: self.menu_advanced.add_checkbutton( label=self.translate("Don't check for update"), variable=self.boolvar_dont_check_for_update, - command=lambda: self.option.edit( + command=lambda: self.common.option.edit( "dont_check_for_update", self.boolvar_dont_check_for_update ) @@ -215,29 +211,36 @@ class Gui: self.menu_conv_process.add_radiobutton( label=self.translate("1 ", "process"), variable=self.intvar_process_track, value=1, - command=lambda: self.option.edit("process_track", 1) + command=lambda: self.common.option.edit("process_track", 1) ) self.menu_conv_process.add_radiobutton( label=self.translate("2 ", "process"), variable=self.intvar_process_track, value=2, - command=lambda: self.option.edit("process_track", 2) + command=lambda: self.common.option.edit("process_track", 2) ) self.menu_conv_process.add_radiobutton( label=self.translate("4 ", "process"), variable=self.intvar_process_track, value=4, - command=lambda: self.option.edit("process_track", 4) + command=lambda: self.common.option.edit("process_track", 4) ) self.menu_conv_process.add_radiobutton( label=self.translate("8 ", "process"), variable=self.intvar_process_track, value=8, - command=lambda: self.option.edit("process_track", 8) + command=lambda: self.common.option.edit("process_track", 8) ) ## 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_command( + label=self.translate("Change track configuration"), + command=self.common.show_gui_track_configuration + ) + + 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) @@ -270,7 +273,7 @@ class Gui: self.menu_help.add_command(label="Discord", command=lambda: webbrowser.open(DISCORD_URL)) def reload_ctconfig(self) -> None: - self.game.ctconfig.load_ctconfig_file( + self.common.ct_config.load_ctconfig_file( ctconfig_file=self.get_ctconfig_path_pack(self.stringvar_ctconfig.get()) ) @@ -326,7 +329,7 @@ class Gui: except requests.ConnectionError: messagebox.showwarning(self.translate("Warning"), self.translate("Can't connect to internet. Download will be disabled.")) - self.option.disable_download = True + self.common.option.disable_download = True except: self.log_error() @@ -339,10 +342,10 @@ class Gui: with open("./error.log", "a") as f: f.write( f"---\n" - f"For game version : {self.game.ctconfig.version}\n" + f"For game version : {self.common.ct_config.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"ctconfig directory : {os.listdir(self.common.ct_config.pack_path)}\n" + f"GAME/files/ information : {self.common.game.path, self.common.game.region}\n" f"{error}\n" ) messagebox.showerror( @@ -404,7 +407,7 @@ class Gui: :param gamelang: force a destination language to convert track :return: translated text """ - lang = gamelang_to_lang.get(gamelang, self.stringvar_language.get()) + lang = gamelang_to_lang.get(gamelang, self.common.option.language) if lang not in translation_dict: return "".join(texts) # if no translation language is found _lang_trad = translation_dict[lang] @@ -434,3 +437,6 @@ class Gui: self.root.quit() self.root.destroy() sys.exit() + + def mainloop(self) -> None: + self.root.mainloop() \ No newline at end of file diff --git a/source/Gui/SelectPack.py b/source/Gui/SelectPack.py new file mode 100644 index 0000000..0bcbf24 --- /dev/null +++ b/source/Gui/SelectPack.py @@ -0,0 +1,3 @@ +class SelectPack: + def __init__(self): + pass \ No newline at end of file diff --git a/source/Gui/TrackSelection.py b/source/Gui/TrackSelection.py new file mode 100644 index 0000000..1feab62 --- /dev/null +++ b/source/Gui/TrackSelection.py @@ -0,0 +1,283 @@ +from tkinter import * +from tkinter import ttk + + +class Orderbox(Listbox): + def order_up(self, *args, **kwargs): + self.order_change(delta_start=1) + + def order_down(self, *args, **kwargs): + self.order_change(delta_end=1) + + def order_change(self, delta_start: int = 0, delta_end: int = 0): + selection = self.curselection() + if len(selection) < 1: return + index = selection[0] + + values = self.get(index - delta_start, index + delta_end) + if len(values) < 2: return + + self.delete(index - delta_start, index + delta_end) + self.insert(index - delta_start, *reversed(values)) + + self.selection_set(index - delta_start + delta_end) + + +class TrackSelection: + def __init__(self, common): + self.common = common + + self.root = Toplevel(self.common.gui_main.root) + self.root.title("Track selection") + self.root.iconbitmap("./icon.ico") + self.root.resizable(False, False) + self.root.grab_set() + + self.text_is_equal_to = "is equal to" + self.text_is_in = "is in" + self.text_is_between = "is between" + self.text_contains = "contains" + + self.text_and = "and" + self.text_nand = "nand" + self.text_or = "or" + self.text_nor = "nor" + self.text_xor = "xor" + self.text_xnor = "xnor" + self.condition_link_end = "end" + + track_filter_row_start = 10 + self.condition_links = { + self.text_and: lambda a, b: lambda track: a(track) and b(track), + self.text_nand: lambda a, b: lambda track: not (a(track) and b(track)), + self.text_or: lambda a, b: lambda track: a(track) or b(track), + self.text_nor: lambda a, b: lambda track: not (a(track) or b(track)), + self.text_xor: lambda a, b: lambda track: a(track) != b(track), + self.text_xnor: lambda a, b: lambda track: a(track) == b(track), + self.condition_link_end: -1 + } + + def del_frame_track_filter(frames_filter: list, index: int = 0): + for elem in frames_filter[index:]: # remove all track filter after this one + elem["frame"].destroy() + del frames_filter[index:] + + def add_frame_track_filter(root: Frame, frames_filter: list, index: int = 0): + frame = Frame(root) + frame.grid(row=index + track_filter_row_start, column=1, sticky="NEWS") + Label(frame, text="If track's").grid(row=1, column=1) + track_property = ttk.Combobox(frame, values=list(self.common.ct_config.get_all_track_possibilities().keys())) + track_property.current(0) + track_property.grid(row=1, column=2) + + frame_equal = Frame(frame) + entry_equal = Entry(frame_equal, width=20) + entry_equal.grid(row=1, column=1) + entry_equal.insert(END, "value") + + frame_in = Frame(frame) + entry_in = Entry(frame_in, width=30) + entry_in.grid(row=1, column=1) + entry_in.insert(END, "value1, value2, ...") + + frame_between = Frame(frame) + entry_start = Entry(frame_between, width=10) + entry_start.grid(row=1, column=1) + entry_start.insert(END, "value1") + Label(frame_between, text="and").grid(row=1, column=2) + entry_end = Entry(frame_between, width=10) + entry_end.insert(END, "value2") + entry_end.grid(row=1, column=3) + + frame_contains = Frame(frame) + entry_contains = Entry(frame_contains, width=20) + entry_contains.grid(row=1, column=1) + entry_contains.insert(END, "value") + + condition_frames = { + self.text_is_equal_to: frame_equal, + self.text_is_in: frame_in, + self.text_is_between: frame_between, + self.text_contains: frame_contains, + } + + def change_condition_type(event: Event = None): + condition = combobox_condition_type.get() + for frame in condition_frames.values(): frame.grid_forget() + condition_frames[condition].grid(row=1, column=10) + + combobox_condition_type = ttk.Combobox(frame, values=list(condition_frames.keys()), width=10) + combobox_condition_type.current(0) + combobox_condition_type.bind("<>", change_condition_type) + change_condition_type() + combobox_condition_type.grid(row=1, column=3) + + def change_condition_link(event: Event = None): + link = next_condition_link.get() + + if link == self.condition_link_end: + del_frame_track_filter(frames_filter, index=index + 1) + + else: + if frames_filter[-1]["frame"] == frame: # if this is the last filter available + add_frame_track_filter(root=root, frames_filter=frames_filter, index=index + 1) + + next_condition_link = ttk.Combobox(frame, values=list(self.condition_links.keys()), width=10) + next_condition_link.bind("<>", change_condition_link) + next_condition_link.set(self.condition_link_end) + next_condition_link.grid(row=1, column=100) + + frames_filter.append({ + "frame": frame, + "track_property": track_property, + "condition_type": combobox_condition_type, + + "value_equal": entry_equal, + "value_in": entry_in, + "value_between_start": entry_start, + "value_between_end": entry_end, + "value_contains": entry_contains, + + "next_condition_link": next_condition_link + }) + + def get_change_enable_track_filter_func(root: [Frame, LabelFrame], frames_filter: list, variable_enable: BooleanVar): + def change_enable_track_filter(event: Event = None): + if variable_enable.get(): add_frame_track_filter(root=root, frames_filter=frames_filter) + else: del_frame_track_filter(frames_filter=frames_filter) + + return change_enable_track_filter + + self.track_sort = LabelFrame(self.root, text="Sort Track") + self.track_sort.grid(row=1, column=1, sticky="NEWS") + + Label(self.track_sort, text="Sort track by : ").grid(row=1, column=1) + self.combobox_track_sort = ttk.Combobox( + self.track_sort, + values=list(self.common.ct_config.get_all_track_possibilities().keys()) + ) + self.combobox_track_sort.grid(row=1, column=2, sticky="NEWS") + self.combobox_track_sort.insert(END, self.common.ct_config.sort_track_attr) + + self.track_filter = LabelFrame(self.root, text="Filter Track") + self.track_filter.grid(row=2, column=1, sticky="NEWS") + + self.variable_enable_track_filter = BooleanVar(value=False) + self.frames_track_filter = [] + self.checkbutton_track_filter = ttk.Checkbutton( + self.track_filter, + text="Enable track filter", + variable=self.variable_enable_track_filter, + command=get_change_enable_track_filter_func( + self.track_filter, + self.frames_track_filter, + self.variable_enable_track_filter + ) + ) + self.checkbutton_track_filter.grid(row=1, column=1) + + self.track_highlight = LabelFrame(self.root, text="Highlight Track") + self.track_highlight.grid(row=3, column=1, sticky="NEWS") + + self.variable_enable_track_highlight = BooleanVar(value=False) + self.frames_track_highlight = [] + self.checkbutton_track_highlight = ttk.Checkbutton( + self.track_highlight, + text="Enable track highlight", + variable=self.variable_enable_track_highlight, + command=get_change_enable_track_filter_func( + self.track_highlight, + self.frames_track_highlight, + self.variable_enable_track_highlight + ) + ) + self.checkbutton_track_highlight.grid(row=1, column=1) + + self.track_random_new = LabelFrame(self.root, text="Overwrite random cup new") + self.track_random_new.grid(row=4, column=1, sticky="NEWS") + + self.variable_enable_track_random_new = BooleanVar(value=False) + self.frames_track_random_new = [] + self.checkbutton_track_random_new = ttk.Checkbutton( + self.track_random_new, + text="Enable overwriting random \"new\" track", + variable=self.variable_enable_track_random_new, + command=get_change_enable_track_filter_func( + self.track_random_new, + self.frames_track_random_new, + self.variable_enable_track_random_new + ) + ) + self.checkbutton_track_random_new.grid(row=1, column=1) + + Button( + self.root, + text="Save configuration", + relief=RIDGE, + command=self.save_configuration + ).grid(row=100, column=1, sticky="E") + + def save_configuration(self): + self.common.ct_config.sort_track_attr = self.combobox_track_sort.get() + self.common.ct_config.filter_track_selection = self.get_filter( + self.variable_enable_track_filter, + self.frames_track_filter + ) + self.common.ct_config.filter_track_highlight = self.get_filter( + self.variable_enable_track_highlight, + self.frames_track_highlight + ) + self.common.ct_config.filter_track_random_new = self.get_filter( + self.variable_enable_track_random_new, + self.frames_track_random_new + ) + + def get_filter(self, condition_enabled: BooleanVar, frames_filter: list): + s = lambda x: str(x).strip() + + filter_condition = lambda track: True + if not condition_enabled.get(): return filter_condition + next_condition_link_func = lambda a, b: lambda track: a(track) and b(track) + + for frame_filter in frames_filter: + track_property = frame_filter["track_property"].get() + + value_equal = frame_filter["value_equal"].get() + value_in = frame_filter["value_in"].get() + value_contains = frame_filter["value_contains"].get() + value_between_start = frame_filter["value_between_start"].get() + value_between_end = frame_filter["value_between_end"].get() + + def _is_between_func_wrapper(property): + def _is_between_func(track): + from_ = s(value_between_start) + to = s(value_between_end) + prop = s(getattr(track, property, None)) + + if from_.isnumeric() and prop.isnumeric() and to.isnumeric(): return int(from_) <= int(prop) <= int(to) + else: return from_ <= prop <= to + + return _is_between_func + + track_conditions_filter = { + self.text_is_equal_to: lambda property: lambda track: + s(getattr(track, property, None)) == s(value_equal), + self.text_is_in: lambda property: lambda track: + s(getattr(track, property, None)) in [s(v) for v in value_in.split(",")], + self.text_is_between: + _is_between_func_wrapper, + self.text_contains: lambda property: lambda track: + s(value_contains) in s(getattr(track, property, None)) + } + + track_condition_type = frame_filter["condition_type"].get() + track_condition_filter = track_conditions_filter[track_condition_type] + + filter_condition = next_condition_link_func( + filter_condition, + track_condition_filter(track_property) + ) + next_condition_link = frame_filter["next_condition_link"].get() + next_condition_link_func = self.condition_links[next_condition_link] + + return filter_condition diff --git a/source/Track.py b/source/Track.py index 5eaed2a..00592f7 100644 --- a/source/Track.py +++ b/source/Track.py @@ -35,10 +35,6 @@ class Track: """ 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(s) :param special: track special slot :param music: track music slot @@ -47,8 +43,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 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 :param tags: a list of tags that correspond to the track @@ -66,7 +60,6 @@ class Track: self.warning = warning # Track bug level (1 = minor, 2 = major) self.version = version self.tags = tags if tags else [] - self.refresh_filename() self._is_in_group = is_in_group @@ -82,30 +75,34 @@ class Track: check if track wu8 sha1 is correct :return: 0 if yes, -1 if no """ - return check_file_sha1(self._wu8_file, self.sha1) + return check_file_sha1(self.get_wu8_file(), self.sha1) def check_szs_sha1(self) -> int: """ check if track szs sha1 is correct :return: 0 if yes, -1 if no """ - return check_file_sha1(self._szs_file, self.sha1) + return check_file_sha1(self.get_szs_file(), self.sha1) + + def get_wu8_file(self): return f"{self._wu8_dir}/{self.sha1}.wu8" + def get_szs_file(self): return f"{self._szs_dir}/{self.sha1}.szs" def convert_wu8_to_szs(self) -> None: """ convert track to szs """ - file_wu8 = f"{self._wu8_dir}/{self.sha1}.wu8" - file_szs = f"{self._szs_dir}/{self.sha1}.szs" - if os.path.exists(file_szs) and os.path.getsize(file_szs) < 1000: - os.remove(file_szs) # File under this size are corrupted + szs_file = self.get_szs_file() + wu8_file = self.get_wu8_file() + + if os.path.exists(szs_file) and os.path.getsize(szs_file) < 1000: + os.remove(szs_file) # File under this size are corrupted if not self.check_szs_sha1(): # if sha1 of track's szs is incorrect or track's szs does not exist - if os.path.exists(file_wu8): + if os.path.exists(wu8_file): szs.normalize( - src_file=file_wu8, - dest_file=file_szs + src_file=wu8_file, + dest_file=szs_file ) else: raise MissingTrackWU8() @@ -116,14 +113,17 @@ class Track: """ return self.author if type(self.author) == str else ", ".join(self.author) - def get_ctfile(self, race=False, *args, **kwargs) -> str: + def get_ctfile(self, race=False, filter_random_new=None, *args, **kwargs) -> str: """ get ctfile text to create CTFILE.txt and RCTFILE.txt + :param filter_random_new: function to decide if the track should be used by the "random new" option :param race: is it a text used for Race_*.szs ? :return: ctfile definition for the track """ track_type = "T" track_flag = 0x00 if self.tag_retro in self.tags else 0x01 + if filter_random_new: track_flag = 0x01 if filter_random_new(self) else 0x00 + if self._is_in_group: track_type = "H" track_flag |= 0x04 @@ -152,13 +152,15 @@ class Track: if tag in tag_list: return tag return "" - def get_track_formatted_name(self, highlight_version: str = None, *args, **kwargs) -> str: + def get_track_formatted_name(self, filter_highlight=None, *args, **kwargs) -> str: """ get the track name with score, color, ... :param ct_config: ct_config for tags configuration - :param highlight_version: if a specific version need to be highlighted. + :param filter_highlight: filter function to decide if the track should be filtered. :return: the name of the track with colored prefix, suffix """ + if not filter_highlight: filter_highlight = lambda track: False + hl_prefix = "" # highlight hl_suffix = "" prefix = self.select_tag(self.prefix_list) # tag prefix @@ -174,8 +176,7 @@ class Track: if 0 < self.warning <= 3: star_prefix = warning_color[self.warning] - if self.since_version == highlight_version: - hl_prefix, hl_suffix = "\\\\c{blue1}", "\\\\c{off}" + if filter_highlight(self): hl_prefix, hl_suffix = "\\\\c{blue1}", "\\\\c{off}" if prefix: prefix = "\\\\c{" + self.tags_color[prefix] + "}" + prefix + "\\\\c{off} " if suffix: suffix = " (\\\\c{" + self.tags_color[suffix] + "}" + suffix + "\\\\c{off})" @@ -190,10 +191,6 @@ class Track: """ return f"{self.select_tag(ct_config.prefix_list)}{self.name}{self.select_tag(ct_config.suffix_list)}" - def refresh_filename(self): - self._wu8_file = f"{self._wu8_dir}/{self.sha1}.wu8" - self._szs_file = f"{self._szs_dir}/{self.sha1}.szs" - def load_from_json(self, track_json: dict): """ load the track from a dictionary @@ -202,8 +199,6 @@ class Track: for key, value in track_json.items(): # load all value in the json as class attribute setattr(self, key, value) - self.refresh_filename() - return self def create_from_track_file(self, track_file: str) -> None: