diff --git a/Pack/MKWFaraphel/essentials/_PATCH/files/Scene/UI/CommonMenu.szs.json b/Pack/MKWFaraphel/essentials/_PATCH/files/Scene/UI/CommonMenu.szs.json index ebe77e1..5597107 100644 --- a/Pack/MKWFaraphel/essentials/_PATCH/files/Scene/UI/CommonMenu.szs.json +++ b/Pack/MKWFaraphel/essentials/_PATCH/files/Scene/UI/CommonMenu.szs.json @@ -1,3 +1,4 @@ { - "regex": "*[!_]?.szs" + "mode": "match", + "match_regex": "*[!_]?.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-JAP.bin.json b/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-JAP.bin.json index aa9ce9e..6d71e2a 100644 --- a/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-JAP.bin.json +++ b/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-JAP.bin.json @@ -1,3 +1,3 @@ { - "if": "{{ getattr(mod_config, 'extension', False) == 'JAP' }}" + "if": "getattr(mod_config, 'extension', False) == 'JAP'" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-KOR.bin.json b/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-KOR.bin.json index 96a56b2..48e61df 100644 --- a/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-KOR.bin.json +++ b/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-KOR.bin.json @@ -1,3 +1,3 @@ { - "if": "{{ getattr(mod_config, 'extension', False) == 'KOR' }}" + "if": "getattr(mod_config, 'extension', False) == 'KOR'" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-PAL.bin.json b/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-PAL.bin.json index 6adf0e9..655183d 100644 --- a/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-PAL.bin.json +++ b/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-PAL.bin.json @@ -1,3 +1,3 @@ { - "if": "{{ getattr(mod_config, 'extension', False) == 'PAL' }}" + "if": "getattr(mod_config, 'extension', False) == 'PAL'" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-USA.bin.json b/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-USA.bin.json index 5c03b00..8281653 100644 --- a/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-USA.bin.json +++ b/Pack/MKWFaraphel/essentials/_PATCH/files/rel/lecode-USA.bin.json @@ -1,3 +1,3 @@ { - "if": "{{ getattr(mod_config, 'extension', False) == 'USA' }}" + "if": "getattr(mod_config, 'extension', False) == 'USA'" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/essentials/_PATCH/files/thp/video.thp.json b/Pack/MKWFaraphel/essentials/_PATCH/files/thp/video.thp.json index 29e4639..7814605 100644 --- a/Pack/MKWFaraphel/essentials/_PATCH/files/thp/video.thp.json +++ b/Pack/MKWFaraphel/essentials/_PATCH/files/thp/video.thp.json @@ -1,3 +1,4 @@ { - "regex": "*/*.thp" + "mode": "match", + "match_regex": "*/*.thp" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/CommonLanguageMenu.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/CommonLanguageMenu.szs.json index 6b7efbd..6dbf875 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/CommonLanguageMenu.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/CommonLanguageMenu.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_*.szs" + "mode": "match", + "match_regex": "*_*.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/CommonMenu.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/CommonMenu.szs.json index ebe77e1..5597107 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/CommonMenu.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/CommonMenu.szs.json @@ -1,3 +1,4 @@ { - "regex": "*[!_]?.szs" + "mode": "match", + "match_regex": "*[!_]?.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_E.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_E.szs.json index aaf99ed..c564ad7 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_E.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_E.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_E.szs" + "mode": "match", + "match_regex": "*_E.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_F.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_F.szs.json index 681d99a..78a1c4b 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_F.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_F.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_F.szs" + "mode": "match", + "match_regex": "*_F.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_G.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_G.szs.json index 535c19c..f3070b7 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_G.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_G.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_G.szs" + "mode": "match", + "match_regex": "*_G.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_I.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_I.szs.json index f321d15..74561fa 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_I.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_I.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_I.szs" + "mode": "match", + "match_regex": "*_I.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_J.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_J.szs.json index 5cfb56a..f230b16 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_J.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_J.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_J.szs" + "mode": "match", + "match_regex": "*_J.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_K.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_K.szs.json index 3e890c0..d5ccca3 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_K.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_K.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_K.szs" + "mode": "match", + "match_regex": "*_K.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_M.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_M.szs.json index 6380f22..b77d192 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_M.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_M.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_M.szs" + "mode": "match", + "match_regex": "*_M.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_Q.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_Q.szs.json index b5faa89..ad85378 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_Q.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_Q.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_Q.szs" + "mode": "match", + "match_regex": "*_Q.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_S.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_S.szs.json index 86aa7b9..aa86155 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_S.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_S.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_S.szs" + "mode": "match", + "match_regex": "*_S.szs" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_U.szs.json b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_U.szs.json index 608ac53..de04cf8 100644 --- a/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_U.szs.json +++ b/Pack/MKWFaraphel/specific/_PATCH/files/Scene/UI/Common_U.szs.json @@ -1,3 +1,4 @@ { - "regex": "*_U.szs" + "mode": "match", + "match_regex": "*_U.szs" } \ No newline at end of file diff --git a/source/gui/install.py b/source/gui/install.py index e197294..c0d6a79 100644 --- a/source/gui/install.py +++ b/source/gui/install.py @@ -251,6 +251,7 @@ class SourceGame(ttk.LabelFrame): self.entry.delete(0, tkinter.END) self.entry.insert(0, str(path.absolute())) + if path.suffix == ".dol": path = path.parent.parent self.master.destination_game.set_path(path.parent) def get_path(self) -> Path: diff --git a/source/mkw/ExtractedGame.py b/source/mkw/ExtractedGame.py new file mode 100644 index 0000000..b8781a1 --- /dev/null +++ b/source/mkw/ExtractedGame.py @@ -0,0 +1,110 @@ +from pathlib import Path +from typing import Generator + +from source.mkw.ModConfig import ModConfig +from source.wt import szs +import json + + +class ExtractedGame: + """ + Class that represents an extracted game + """ + + def __init__(self, path: Path | str): + self.path = Path(path) + + def extract_autoadd(self, destination_path: Path | str) -> Generator[dict, None, None]: + """ + Extract all the autoadd files from the game to destination_path + :param destination_path: directory where the autoadd files will be extracted + :return: directory where the autoadd files were extracted + """ + yield {"description": "Extracting autoadd files...", "determinate": False} + szs.autoadd(self.path / "files/Race/Course/", destination_path) + + def install_mystuff(self) -> Generator[dict, None, None]: + """ + Install mystuff directory + :return: + """ + yield {"description": "Installing MyStuff directory...", "determinate": False} + ... + + def install_file(self, mod_config: ModConfig, patch_directory: Path | str, subfile: Path | str) \ + -> Generator[dict, None, None]: + """ + Install a file into the game + :param patch_directory: patch_directory where the subfile is located + :param subfile: subfile to install + :param mod_config: the mod to install + """ + subfile = Path(subfile) + yield {"description": f"Patch {patch_directory.name}\nInstalling {subfile.name}...", "determinate": False} + + ''' + configuration = { + "base": "/files/test.json", # path a another json file to use as a base + + "mode": "copy", # copy, replace, ignore, edit the subfile [default: copy] + # if edit is set, use the file in the extracted game as a source + # replace can't be used inside szs + + "replace_regex": None, # regex expression to match the file on the game to replace + "if": "True", # safe eval expression to check if the file should be installed + + "operation": { # other operation for the file + "img-generate": { + # width, height, default color, ... can be determined from the base file + "format": "RGB", # type of the image + "layers": [ + {"type": "image", ...}, + {"type": "text", ...} + ] + } + + "tpl-encode": {"encoding": "TPL.RGB565"}, # encode an image to a tpl with the given format + + "bmg-replace": { + "mode": "regex" # regex or id + "template": { + "CWF": "{{ ONLINE_SERVICE }}", # regex type expression + "0x203F": "{{ ONLINE_SERVICE }}" # id type expression + } + } + } + } + ''' + + configuration = { # default configuration + "mode": "copy", + "if": "True", + } + configuration_path = subfile.with_suffix(subfile.suffix + ".json") + if configuration_path.exists(): configuration |= json.loads(configuration_path.read_text(encoding="utf8")) + + def install_patch(self, mod_config: ModConfig, patch_directory: Path | str) -> Generator[dict, None, None]: + """ + Install a patch into the game + :param mod_config: the mod to install + :param patch_directory: directory containing the patch + """ + patch_directory = Path(patch_directory) + yield {"description": f"Installing Patch {patch_directory.parent.name}...", "determinate": False} + + for subfile in filter(lambda sf: sf.suffix == ".json", patch_directory.glob("*")): + self.install_file(mod_config, patch_directory, subfile) + + def install_all_patch(self, mod_config: ModConfig) -> Generator[dict, None, None]: + """ + Install all patchs of the mod_config into the game + :param mod_config: the mod to install + :return: + """ + yield {"description": "Installing all Patch...", "determinate": False} + + # for all directory that are in the root of the mod, and don't start with an underscore, + # for all the subdirectory named "_PATCH", apply the patch + for part_directory in mod_config.get_mod_directory().glob("[!_]*"): + for patch_directory in part_directory.glob("_PATCH/"): + self.install_patch(mod_config, patch_directory) diff --git a/source/mkw/Game.py b/source/mkw/Game.py index 5e0cf8b..2594bd9 100644 --- a/source/mkw/Game.py +++ b/source/mkw/Game.py @@ -1,112 +1,21 @@ -import json from pathlib import Path from typing import Generator +from source.mkw.ExtractedGame import ExtractedGame from source.mkw.ModConfig import ModConfig -from source.wt import szs from source.wt.wit import WITPath, Region, Extension -class ExtractedGame: - """ - Class that represents an extracted game - """ +class NotMKWGameError(Exception): def __init__(self, path: Path | str): - self.path = Path(path) + path = Path(path) + super().__init__(f'Not a Mario Kart Wii game : "{path.name}"') - def extract_autoadd(self, destination_path: Path | str) -> Generator[dict, None, None]: - """ - Extract all the autoadd files from the game to destination_path - :param destination_path: directory where the autoadd files will be extracted - :return: directory where the autoadd files were extracted - """ - yield {"description": "Extracting autoadd files...", "determinate": False} - szs.autoadd(self.path / "files/Race/Course/", destination_path) - def install_mystuff(self) -> Generator[dict, None, None]: - """ - Install mystuff directory - :return: - """ - yield {"description": "Installing MyStuff directory...", "determinate": False} - ... - - def install_file(self, mod_config: ModConfig, patch_directory: Path | str, subfile: Path | str) \ - -> Generator[dict, None, None]: - """ - Install a file into the game - :param patch_directory: patch_directory where the subfile is located - :param subfile: subfile to install - :param mod_config: the mod to install - """ - subfile = Path(subfile) - yield {"description": f"Patch {patch_directory.name}\nInstalling {subfile.name}...", "determinate": False} - - ''' - configuration = { - "base": "/files/test.json", # path a another json file to use as a base - - "mode": "copy", # copy, replace, ignore, edit the subfile - # if edit is set, use the file in the extracted game as a source - - "replace_regex": None, # regex expression to match the file on the game to replace - "if": "True", # safe eval expression to check if the file should be installed - - "operation": { # other operation for the file - "img-generate": { - # width, height, default color, ... can be determined from the base file - "format": "RGB", # type of the image - "layers": [ - {"type": "image", ...}, - {"type": "text", ...} - ] - } - - "tpl-encode": {"encoding": "TPL.RGB565"}, # encode an image to a tpl with the given format - - "bmg-replace": { - "mode": "regex" # regex, bmg id - "if": "True" # safe eval expression to check if the bmg operation should be installed - "template": { - "CWF": "{{ ONLINE_SERVICE }}", # regex type expression - "0x203F": "{{ ONLINE_SERVICE }}" # bmg id type expression - } - } - } - } - ''' - - configuration = { # default configuration - "mode": "copy", - "if": "True", - } - configuration_path = subfile.with_suffix(subfile.suffix + ".json") - if configuration_path.exists(): configuration |= json.loads(configuration_path.read_text(encoding="utf8")) - - def install_patch(self, mod_config: ModConfig, patch_directory: Path | str) -> Generator[dict, None, None]: - """ - Install a patch into the game - :param mod_config: the mod to install - :param patch_directory: directory containing the patch - """ - patch_directory = Path(patch_directory) - yield {"description": f"Installing Patch {patch_directory.parent.name}...", "determinate": False} - - for subfile in filter(lambda sf: sf.suffix == ".json", patch_directory.glob("*")): - self.install_file(mod_config, patch_directory, subfile) - - def install_all_patch(self, mod_config: ModConfig) -> Generator[dict, None, None]: - """ - Install all patchs of the mod_config into the game - :param mod_config: the mod to install - :return: - """ - yield {"description": "Installing all Patch...", "determinate": False} - - # for all directory that are in the root of the mod, and don't start with an underscore, - # for all of the subdirectory named "_PATCH", apply the patch - for patch_directory in mod_config.get_mod_directory().glob("[!_]*").rglob("_PATCH/"): - self.install_patch(mod_config, patch_directory) +class NotVanillaError(Exception): + def __init__(self, path: Path | str): + path = Path(path) + super().__init__(f'This game is already modded : "{path.name}"') class Game: @@ -118,7 +27,7 @@ class Game: Return True if the game is Mario Kart Wii, False otherwise :return: is the game a MKW game """ - return self.wit_path.analyze()["dol_is_mkw"] == 1 + return self.wit_path.analyze().get("dol_is_mkw") == 1 def is_vanilla(self) -> bool: """ @@ -133,17 +42,29 @@ class Game: :param dest: destination directory """ gen = self.wit_path.progress_extract_all(dest) - for gen_data in gen: - yield { - "description": f'EXTRACTING - {gen_data["percentage"]}% - (estimated time remaining: ' - f'{gen_data["estimation"] if gen_data["estimation"] is not None else "-:--"})', - "maximum": 100, - "value": gen_data["percentage"], - "determinate": True - } - try: next(gen) - except StopIteration as e: - return e.value + + if self.wit_path.extension == Extension.FST: + for gen_data in gen: + yield { + "description": "Copying Game...", + "determinate": False + } + try: next(gen) + except StopIteration as e: + return e.value + + else: + for gen_data in gen: + yield { + "description": f'Extracting - {gen_data["percentage"]}% - (estimated time remaining: ' + f'{gen_data["estimation"] if gen_data["estimation"] is not None else "-:--"})', + "maximum": 100, + "value": gen_data["percentage"], + "determinate": True + } + try: next(gen) + except StopIteration as e: + return e.value @staticmethod def get_output_directory(dest: Path | str, mod_config: ModConfig) -> Path: @@ -180,6 +101,9 @@ class Game: # get the directory where the game will be extracted extracted_game = ExtractedGame(self.get_output_directory(dest, mod_config)) + if not self.is_mkw(): raise NotMKWGameError(self.wit_path.path) + if not self.is_vanilla(): raise NotVanillaError(self.wit_path.path) + yield from self.extract(extracted_game.path) yield from extracted_game.extract_autoadd(cache_directory / "autoadd/") yield from extracted_game.install_mystuff() diff --git a/source/wt/wit.py b/source/wt/wit.py index 03175db..ce4ea5f 100644 --- a/source/wt/wit.py +++ b/source/wt/wit.py @@ -146,18 +146,25 @@ class WITPath: :param dest: destination directory :return: the extracted file path """ - process = self._run_popen("EXTRACT", self.path, "-d", dest, "--progress") + if self.extension == Extension.FST: + yield { + "determinate": False + } + shutil.copytree(self._get_fst_root(), dest) - while process.poll() is None: - m = re.match(r'\s*(?P\d*)%(?:.*?ETA (?P\d*:\d*))?\s*', process.stdout.readline()) - if m: - yield { - "percentage": int(m.group("percentage")), - "estimation": m.group("estimation") - } + else: + process = self._run_popen("EXTRACT", self.path, "-d", dest, "--progress") - if process.returncode != 0: - raise WTError(tools_path, process.returncode) + while process.poll() is None: + m = re.match(r'\s*(?P\d*)%(?:.*?ETA (?P\d*:\d*))?\s*', process.stdout.readline()) + if m: + yield { + "percentage": int(m.group("percentage")), + "estimation": m.group("estimation") + } + + if process.returncode != 0: + raise WTError(tools_path, process.returncode) return dest