diff --git a/Pack/MKWFaraphel/_CUPS/GBA/GBA1.png b/Pack/MKWFaraphel/_CUPS/GBA/1.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/GBA/GBA1.png rename to Pack/MKWFaraphel/_CUPS/GBA/1.png diff --git a/Pack/MKWFaraphel/_CUPS/GBA/GBA2.png b/Pack/MKWFaraphel/_CUPS/GBA/2.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/GBA/GBA2.png rename to Pack/MKWFaraphel/_CUPS/GBA/2.png diff --git a/Pack/MKWFaraphel/_CUPS/GBA/GBA3.png b/Pack/MKWFaraphel/_CUPS/GBA/3.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/GBA/GBA3.png rename to Pack/MKWFaraphel/_CUPS/GBA/3.png diff --git a/Pack/MKWFaraphel/_CUPS/GBA/GBA4.png b/Pack/MKWFaraphel/_CUPS/GBA/4.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/GBA/GBA4.png rename to Pack/MKWFaraphel/_CUPS/GBA/4.png diff --git a/Pack/MKWFaraphel/_CUPS/GBA/GBA5.png b/Pack/MKWFaraphel/_CUPS/GBA/5.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/GBA/GBA5.png rename to Pack/MKWFaraphel/_CUPS/GBA/5.png diff --git a/Pack/MKWFaraphel/_CUPS/GCN/GCN1.png b/Pack/MKWFaraphel/_CUPS/GCN/1.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/GCN/GCN1.png rename to Pack/MKWFaraphel/_CUPS/GCN/1.png diff --git a/Pack/MKWFaraphel/_CUPS/GCN/GCN2.png b/Pack/MKWFaraphel/_CUPS/GCN/2.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/GCN/GCN2.png rename to Pack/MKWFaraphel/_CUPS/GCN/2.png diff --git a/Pack/MKWFaraphel/_CUPS/GCN/GCN3.png b/Pack/MKWFaraphel/_CUPS/GCN/3.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/GCN/GCN3.png rename to Pack/MKWFaraphel/_CUPS/GCN/3.png diff --git a/Pack/MKWFaraphel/_CUPS/GCN/GCN4.png b/Pack/MKWFaraphel/_CUPS/GCN/4.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/GCN/GCN4.png rename to Pack/MKWFaraphel/_CUPS/GCN/4.png diff --git a/Pack/MKWFaraphel/_CUPS/MKT/MKT1.png b/Pack/MKWFaraphel/_CUPS/MKT/1.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/MKT/MKT1.png rename to Pack/MKWFaraphel/_CUPS/MKT/1.png diff --git a/Pack/MKWFaraphel/_CUPS/MKT/MKT2.png b/Pack/MKWFaraphel/_CUPS/MKT/2.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/MKT/MKT2.png rename to Pack/MKWFaraphel/_CUPS/MKT/2.png diff --git a/Pack/MKWFaraphel/_CUPS/MKT/MKT3.png b/Pack/MKWFaraphel/_CUPS/MKT/3.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/MKT/MKT3.png rename to Pack/MKWFaraphel/_CUPS/MKT/3.png diff --git a/Pack/MKWFaraphel/_CUPS/N64/N641.png b/Pack/MKWFaraphel/_CUPS/N64/1.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/N64/N641.png rename to Pack/MKWFaraphel/_CUPS/N64/1.png diff --git a/Pack/MKWFaraphel/_CUPS/N64/N642.png b/Pack/MKWFaraphel/_CUPS/N64/2.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/N64/N642.png rename to Pack/MKWFaraphel/_CUPS/N64/2.png diff --git a/Pack/MKWFaraphel/_CUPS/N64/N643.png b/Pack/MKWFaraphel/_CUPS/N64/3.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/N64/N643.png rename to Pack/MKWFaraphel/_CUPS/N64/3.png diff --git a/Pack/MKWFaraphel/_CUPS/SNES/SNES1.png b/Pack/MKWFaraphel/_CUPS/SNES/1.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/SNES/SNES1.png rename to Pack/MKWFaraphel/_CUPS/SNES/1.png diff --git a/Pack/MKWFaraphel/_CUPS/SNES/SNES2.png b/Pack/MKWFaraphel/_CUPS/SNES/2.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/SNES/SNES2.png rename to Pack/MKWFaraphel/_CUPS/SNES/2.png diff --git a/Pack/MKWFaraphel/_CUPS/SNES/SNES3.png b/Pack/MKWFaraphel/_CUPS/SNES/3.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/SNES/SNES3.png rename to Pack/MKWFaraphel/_CUPS/SNES/3.png diff --git a/Pack/MKWFaraphel/_CUPS/SNES/SNES4.png b/Pack/MKWFaraphel/_CUPS/SNES/4.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/SNES/SNES4.png rename to Pack/MKWFaraphel/_CUPS/SNES/4.png diff --git a/Pack/MKWFaraphel/_CUPS/SNES/SNES5.png b/Pack/MKWFaraphel/_CUPS/SNES/5.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/SNES/SNES5.png rename to Pack/MKWFaraphel/_CUPS/SNES/5.png diff --git a/Pack/MKWFaraphel/_CUPS/WII U/Switch1.png b/Pack/MKWFaraphel/_CUPS/Switch/1.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/WII U/Switch1.png rename to Pack/MKWFaraphel/_CUPS/Switch/1.png diff --git a/Pack/MKWFaraphel/_CUPS/WII U/Switch2.png b/Pack/MKWFaraphel/_CUPS/Switch/2.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/WII U/Switch2.png rename to Pack/MKWFaraphel/_CUPS/Switch/2.png diff --git a/Pack/MKWFaraphel/_CUPS/WII U/Switch3.png b/Pack/MKWFaraphel/_CUPS/Switch/3.png similarity index 100% rename from Pack/MKWFaraphel/_CUPS/WII U/Switch3.png rename to Pack/MKWFaraphel/_CUPS/Switch/3.png diff --git a/Pack/MKWFaraphel/mod_config.json b/Pack/MKWFaraphel/mod_config.json index c0b8257..b9b7ef7 100644 --- a/Pack/MKWFaraphel/mod_config.json +++ b/Pack/MKWFaraphel/mod_config.json @@ -44,7 +44,7 @@ "SK64": "green", "SMG": "red2", "Spyro 1": "blue", - "Wii U": "red4", + "Switch": "red4", "Wii": "blue", "3DS": "YOR3", "DS": "white", @@ -65,7 +65,7 @@ "tags_suffix": { "Boost": "orange" }, - "tags_cups": ["Wii U", "3DS", "DS", "GCN", "GBA", "N64", "SNES", "MKT", "RMX", "DX", "GP"], + "tags_cups": ["Switch", "3DS", "DS", "GCN", "GBA", "N64", "SNES", "MKT", "RMX", "DX", "GP"], "track_formatting": { "menu_name": "{{ ('\\c{YOR2}\\x'+hex(65296+getattr(track, 'score'))[2:]+'\\{off} ') if hasattr(track, 'score') else '' }}{{ (prefix+' ') if prefix else '' }}{{ getattr(track, 'name', '') }}{{ (' ('+suffix +')') if suffix else '' }}", @@ -92,7 +92,7 @@ "score":3, "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -106,7 +106,7 @@ "version":"RC1", "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -120,7 +120,7 @@ "version":"v1.2b", "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -134,7 +134,7 @@ "version":"v1", "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -148,7 +148,7 @@ "version":"RC1", "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -162,7 +162,7 @@ "score":4, "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -176,7 +176,7 @@ "score":3, "tags":[ "Retro", - "Wii U" + "Switch" ], "note":"some collision (like the plane) are buggy, and the texture feel a bit too dark" }, @@ -191,7 +191,7 @@ "version":"v1.1", "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -205,7 +205,7 @@ "score":5, "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -219,7 +219,7 @@ "score":5, "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -233,7 +233,7 @@ "score":5, "tags":[ "Retro", - "Wii U" + "Switch" ] }, { @@ -245,7 +245,7 @@ "since_version":"0.1", "tags":[ "Retro", - "Wii U" + "Switch" ], "sha1":"e782de12471731e9bb155eea6872cd5b2303357d", "version":"v1.3" diff --git a/source/mkw/Cup.py b/source/mkw/Cup.py index f5c7b5e..07a4864 100644 --- a/source/mkw/Cup.py +++ b/source/mkw/Cup.py @@ -1,32 +1,64 @@ # class that represent a mario kart wii cup -from PIL import Image +from PIL import Image, ImageDraw, ImageFont class Cup: - __slots__ = ["_tracks", "cup_id"] + __slots__ = ["_tracks", "cup_name", "cup_id"] _last_cup_id = 0 - def __init__(self, tracks: list["Track | TrackGroup"], cup_id: str | None = None): + def __init__(self, tracks: list["Track | TrackGroup"], cup_name: str | None = None): self._tracks = tracks[:4] - if cup_id is None: - cup_id = self.__class__._last_cup_id - self.__class__._last_cup_id += 1 - - self.cup_id = cup_id + self.cup_id = self.__class__._last_cup_id + self.cup_name = cup_name if cup_name is not None else self.cup_id + self.__class__._last_cup_id += 1 def __repr__(self): - return f"" + return f"" - def get_cup_icon(self) -> Image.Image: - ... + def get_default_cticon(self, mod_config: "ModConfig") -> Image.Image: + """ + Get the default cticon for this cup + :return: the default cticon + """ + ct_icon = Image.new("RGBA", (128, 128)) + draw = ImageDraw.Draw(ct_icon) + + draw.text( + (4, 4), + "CT", + (255, 165, 0), + font=ImageFont.truetype(mod_config.get_default_font(), 90), + stroke_width=2, + stroke_fill=(0, 0, 0) + ) + draw.text( + (5, 80), + f"{self.cup_id:03}", + (255, 165, 0), + font=ImageFont.truetype(mod_config.get_default_font(), 60), + stroke_width=2, + stroke_fill=(0, 0, 0) + ) + + return ct_icon + + def get_cticon(self, mod_config: "ModConfig") -> Image.Image: + """ + Get the cticon for this cup + :return: the cticon + """ + path = mod_config.get_mod_directory() / f"_CUPS/{self.cup_name}.png" + if path.exists(): return Image.open(path) + # if the icon doesn't exist, use the default automatically generated one + return self.get_default_cticon(mod_config=mod_config) def get_ctfile(self, mod_config: "ModConfig") -> str: """ Get the ctfile for this cup :return: the ctfile """ - ctfile = f'C "{self.cup_id}"\n' + ctfile = f'C "{self.cup_name}"\n' for track in self._tracks: ctfile += track.get_ctfile(mod_config=mod_config) ctfile += "\n" diff --git a/source/mkw/Game.py b/source/mkw/Game.py index 63d438f..43c87c3 100644 --- a/source/mkw/Game.py +++ b/source/mkw/Game.py @@ -1,3 +1,4 @@ +import json from pathlib import Path from typing import Generator @@ -6,25 +7,69 @@ from source.wt import szs from source.wt.wit import WITPath, Region, Extension -def extract_autoadd(extracted_game: Path | str, destination_path: Path | str) -> Path: +class ExtractedGame: """ - Extract all the autoadd files from the game to destination_path - :param extracted_game: path of the extracted game - :param destination_path: directory where the autoadd files will be extracted - :return: directory where the autoadd files were extracted + Class that represents an extracted game """ - yield {"description": "Extracting autoadd files...", "determinate": False} - szs.autoadd(extracted_game / "files/Race/Course/", destination_path) + 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(extracted_game: Path | str) -> None: - """ - Install mystuff directory - :param extracted_game: the extracted game - :return: - """ - yield {"description": "Installing MyStuff directory...", "determinate": False} + 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"Installing {subfile.name}...", "determinate": False} + + configuration = {} + 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.rglob("*")): + self.install_file(mod_config, 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 Game: @@ -63,9 +108,11 @@ class Game: except StopIteration as e: return e.value - def get_output_directory(self, dest: Path | str, mod_config: ModConfig) -> Path: + @staticmethod + def get_output_directory(dest: Path | str, mod_config: ModConfig) -> Path: """ Return the directory where the game will be installed + :param dest: destination directory :param mod_config: mod configuration :return: directory where the game will be installed """ @@ -94,9 +141,10 @@ class Game: cache_directory.mkdir(parents=True, exist_ok=True) # get the directory where the game will be extracted - extracted_game: Path = self.get_output_directory(dest, mod_config) + extracted_game = ExtractedGame(self.get_output_directory(dest, mod_config)) - yield from self.extract(extracted_game) - yield from extract_autoadd(extracted_game, cache_directory / "autoadd/") - yield from install_mystuff(extracted_game) + yield from self.extract(extracted_game.path) + yield from extracted_game.extract_autoadd(cache_directory / "autoadd/") + yield from extracted_game.install_mystuff() + yield from extracted_game.install_all_patch(mod_config) diff --git a/source/mkw/ModConfig.py b/source/mkw/ModConfig.py index 451fc92..ad0a5a1 100644 --- a/source/mkw/ModConfig.py +++ b/source/mkw/ModConfig.py @@ -1,6 +1,8 @@ from pathlib import Path from typing import Generator +from PIL import Image + from source.mkw import Tag, Color from source.mkw.Cup import Cup from source.mkw.Track import Track @@ -9,11 +11,11 @@ import json # representation of the configuration of a mod class ModConfig: - __slots__ = ("name", "nickname", "variant", "region", "tags_prefix", "tags_suffix", + __slots__ = ("name", "path", "nickname", "variant", "region", "tags_prefix", "tags_suffix", "default_track", "_tracks", "version", "original_track_prefix", "swap_original_order", "keep_original_track", "enable_random_cup", "tags_cups", "track_formatting") - def __init__(self, name: str, nickname: str = None, version: str = None, variant: str = None, + def __init__(self, path: Path | str, name: str, nickname: str = None, version: str = None, variant: str = None, tags_prefix: dict[Tag, Color] = None, tags_suffix: dict[Tag, Color] = None, tags_cups: list[Tag] = None, region: dict[int] | int = None, default_track: "Track | TrackGroup" = None, tracks: list["Track | TrackGroup"] = None, @@ -21,6 +23,8 @@ class ModConfig: keep_original_track: bool = None, enable_random_cup: bool = None, track_formatting: dict[str, str] = None): + self.path = Path(path) + self.name: str = name self.nickname: str = nickname if nickname is not None else name self.version: str = version if version is not None else "v1.0.0" @@ -48,20 +52,22 @@ class ModConfig: return f"" @classmethod - def from_dict(cls, config_dict: dict) -> "ModConfig": + def from_dict(cls, path: Path | str, config_dict: dict) -> "ModConfig": """ Create a ModConfig from a dict + :param path: path of the mod_config.json :param config_dict: dict containing the configuration :return: ModConfig """ kwargs = { attr: config_dict.get(attr) for attr in cls.__slots__ - if attr not in ["name", "default_track", "_tracks", "tracks"] + if attr not in ["name", "default_track", "_tracks", "tracks", "path"] # these keys are treated after or are reserved } return cls( + path=Path(path), name=config_dict["name"], **kwargs, @@ -77,8 +83,18 @@ class ModConfig: :param config_file: file containing the configuration :return: ModConfig """ - if isinstance(config_file, str): config_file = Path(config_file) - return cls.from_dict(json.loads(config_file.read_text(encoding="utf8"))) + config_file = Path(config_file) + return cls.from_dict( + path=config_file, + config_dict=json.loads(config_file.read_text(encoding="utf8")) + ) + + def get_mod_directory(self) -> Path: + """ + Get the directory of the mod + :return: directory of the mod + """ + return self.path.parent def get_tracks(self) -> Generator["Track", None, None]: """ @@ -105,13 +121,13 @@ class ModConfig: if len(track_buffer) > 4: current_tag_count += 1 - yield Cup(tracks=track_buffer, cup_id=f"{current_tag_name}-{current_tag_count}") + yield Cup(tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count}") track_buffer = [] # if there is still tracks in the buffer, create a cup with them and fill with default> if len(track_buffer) > 0: track_buffer.extend([self.default_track] * (4 - len(track_buffer))) - yield Cup(tracks=track_buffer, cup_id=f"{current_tag_name}-{current_tag_count+1}") + yield Cup(tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count+1}") def get_unordered_cups(self) -> Generator["Cup", None, None]: """ @@ -167,3 +183,61 @@ class ModConfig: ctfile += cup.get_ctfile(mod_config=self) return ctfile + + def get_base_cticons(self) -> Generator[Image.Image, None, None]: + """ + Return the base cticon + :return: + """ + icon_names = ["left", "right"] + + if self.keep_original_track: + icon_names += [ + f"_DEFAULT/{name}" + for name in ( + ["mushroom", "shell", "flower", "banana", "star", "leaf", "special", "lightning"] + if self.swap_original_order else + ["mushroom", "flower", "star", "special", "shell", "banana", "leaf", "lightning"] + ) + ] + if self.enable_random_cup: icon_names.append("random") + + for icon_name in icon_names: + yield Image.open(self.get_mod_directory() / f"{icon_name}.png").resize((128, 128)) + + def get_custom_cticons(self) -> Generator[Image.Image, None, None]: + """ + Return the custom ct_icon generated from the ModConfig + :return: + """ + for cup in self.get_cups(): + yield cup.get_cticon(mod_config=self).resize((128, 128)) + + def get_cticons(self) -> Generator[Image.Image, None, None]: + """ + Return all the ct_icon generated from the ModConfig + :return: + """ + yield from self.get_base_cticons() + yield from self.get_custom_cticons() + + @staticmethod + def get_default_font() -> Path: + """ + Return the default font for creating ct_icons + :return: the path to the default font file + """ + # TODO: make it customizable + return Path("./assets/SuperMario256.ttf") + + def get_full_cticon(self) -> Image.Image: + """ + Return the full ct_icon generated from the ModConfig + :return: + """ + cticons = list(self.get_cticons()) + + full_cticon = Image.new("RGBA", (128 * len(cticons), 128)) + for i, cticon in enumerate(cticons): full_cticon.paste(cticon, (i * 128, 0)) + + return full_cticon