diff --git a/Pack/MKWFaraphel/mod_config.json b/Pack/MKWFaraphel/mod_config.json index 139ff62..58944a8 100644 --- a/Pack/MKWFaraphel/mod_config.json +++ b/Pack/MKWFaraphel/mod_config.json @@ -33,62 +33,76 @@ }, "lpar_template": "{{ mode if (mode := ## SETTINGS_MODE ##) is not None else 'normal' }}.lpar", - "tags_prefix": { - "MSRDS": "{{ bmg_color_text('green', tag) }}", - "CTR": "{{ bmg_color_text('orange', tag) }}", - "CTTR": "{{ bmg_color_text('dark orange', tag) }}", - "CNR": "{{ bmg_color_text('orange', tag) }}", - "DKR": "{{ bmg_color_text('dark red', tag) }}", - "LCP": "{{ bmg_color_text('green', tag) }}", - "LEGO-R": "{{ bmg_color_text('light red', tag) }}", - "MP9": "{{ bmg_color_text('neon yellow', tag) }}", - "MSUSA": "{{ bmg_color_text('green', tag) }}", - "FZMV": "{{ bmg_color_text('yellow', tag) }}", - "KAR": "{{ bmg_color_text('green', tag) }}", - "KO": "{{ bmg_color_text('dark orange', tag) }}", - "FZ": "{{ bmg_color_text('yellow', tag) }}", - "RV": "{{ bmg_color_text('white', tag) }}", - "SADX": "{{ bmg_color_text('dark blue', tag) }}", - "SCR": "{{ bmg_color_text('yellow', tag) }}", - "SH": "{{ bmg_color_text('pink', tag) }}", - "SM64": "{{ bmg_color_text('pink', tag) }}", - "SMB1": "{{ bmg_color_text('light red', tag) }}", - "SMB2": "{{ bmg_color_text('red', tag) }}", - "SSBB": "{{ bmg_color_text('vivid red', tag) }}", - "SMS": "{{ bmg_color_text('dark red', tag) }}", - "SMO": "{{ bmg_color_text('apple red', tag) }}", - "VVVVVV": "{{ bmg_color_text('azure', tag) }}", - "WF": "{{ bmg_color_text('green', tag) }}", - "WP": "{{ bmg_color_text('neon yellow 2', tag) }}", - "Zelda OoT": "{{ bmg_color_text('green', tag) }}", - "Zelda TP": "{{ bmg_color_text('green', tag) }}", - "Zelda WW": "{{ bmg_color_text('green', tag) }}", - "PMWR": "{{ bmg_color_text('neon yellow 2', tag) }}", - "SHR": "{{ bmg_color_text('green', tag) }}", - "SK64": "{{ bmg_color_text('green', tag) }}", - "SMG": "{{ bmg_color_text('light red', tag) }}", - "Spyro 1": "{{ bmg_color_text('azure', tag) }}", - "Switch": "{{ bmg_color_text('vivid red', tag) }}", - "Wii": "{{ bmg_color_text('azure', tag) }}", - "3DS": "{{ bmg_color_text('light orange', tag) }}", - "DS": "{{ bmg_color_text('white', tag) }}", - "GCN": "{{ bmg_color_text('azure', tag) }}", - "GBA": "{{ bmg_color_text('dark blue', tag) }}", - "N64": "{{ bmg_color_text('pink', tag) }}", - "SNES": "{{ bmg_color_text('green', tag) }}", - "RMX": "{{ bmg_color_text('orange', tag) }}", - "MKT": "{{ bmg_color_text('dark orange', tag) }}", - "GP": "{{ bmg_color_text('dark red', tag) }}", - "UT": "{{ bmg_color_text('red', tag) }}", - "GK2": "{{ bmg_color_text('green', tag) }}", - "GK3": "{{ bmg_color_text('green', tag) }}", - "GK7": "{{ bmg_color_text('green', tag) }}", - "FGKR": "{{ bmg_color_text('dark orange', tag) }}" + "tags_templates": { + "prefix": { + "MSRDS": "{{ bmg_color_text('green', tag) }}", + "CTR": "{{ bmg_color_text('orange', tag) }}", + "CTTR": "{{ bmg_color_text('dark orange', tag) }}", + "CNR": "{{ bmg_color_text('orange', tag) }}", + "DKR": "{{ bmg_color_text('dark red', tag) }}", + "LCP": "{{ bmg_color_text('green', tag) }}", + "LEGO-R": "{{ bmg_color_text('light red', tag) }}", + "MP9": "{{ bmg_color_text('neon yellow', tag) }}", + "MSUSA": "{{ bmg_color_text('green', tag) }}", + "FZMV": "{{ bmg_color_text('yellow', tag) }}", + "KAR": "{{ bmg_color_text('green', tag) }}", + "KO": "{{ bmg_color_text('dark orange', tag) }}", + "FZ": "{{ bmg_color_text('yellow', tag) }}", + "RV": "{{ bmg_color_text('white', tag) }}", + "SADX": "{{ bmg_color_text('dark blue', tag) }}", + "SCR": "{{ bmg_color_text('yellow', tag) }}", + "SH": "{{ bmg_color_text('pink', tag) }}", + "SM64": "{{ bmg_color_text('pink', tag) }}", + "SMB1": "{{ bmg_color_text('light red', tag) }}", + "SMB2": "{{ bmg_color_text('red', tag) }}", + "SSBB": "{{ bmg_color_text('vivid red', tag) }}", + "SMS": "{{ bmg_color_text('dark red', tag) }}", + "SMO": "{{ bmg_color_text('apple red', tag) }}", + "VVVVVV": "{{ bmg_color_text('azure', tag) }}", + "WF": "{{ bmg_color_text('green', tag) }}", + "WP": "{{ bmg_color_text('neon yellow 2', tag) }}", + "Zelda OoT": "{{ bmg_color_text('green', tag) }}", + "Zelda TP": "{{ bmg_color_text('green', tag) }}", + "Zelda WW": "{{ bmg_color_text('green', tag) }}", + "PMWR": "{{ bmg_color_text('neon yellow 2', tag) }}", + "SHR": "{{ bmg_color_text('green', tag) }}", + "SK64": "{{ bmg_color_text('green', tag) }}", + "SMG": "{{ bmg_color_text('light red', tag) }}", + "Spyro 1": "{{ bmg_color_text('azure', tag) }}", + "Switch": "{{ bmg_color_text('vivid red', tag) }}", + "Wii": "{{ bmg_color_text('azure', tag) }}", + "3DS": "{{ bmg_color_text('light orange', tag) }}", + "DS": "{{ bmg_color_text('white', tag) }}", + "GCN": "{{ bmg_color_text('azure', tag) }}", + "GBA": "{{ bmg_color_text('dark blue', tag) }}", + "N64": "{{ bmg_color_text('pink', tag) }}", + "SNES": "{{ bmg_color_text('green', tag) }}", + "RMX": "{{ bmg_color_text('orange', tag) }}", + "MKT": "{{ bmg_color_text('dark orange', tag) }}", + "GP": "{{ bmg_color_text('dark red', tag) }}", + "UT": "{{ bmg_color_text('red', tag) }}", + "GK2": "{{ bmg_color_text('green', tag) }}", + "GK3": "{{ bmg_color_text('green', tag) }}", + "GK7": "{{ bmg_color_text('green', tag) }}", + "FGKR": "{{ bmg_color_text('dark orange', tag) }}" + }, + "suffix": { + "Boost": "{{ bmg_color_text('orange', tag) }}" + } }, - "tags_suffix": { - "Boost": "{{ bmg_color_text('orange', tag) }}" - }, - "tags_cups": ["Switch", "3DS", "DS", "GCN", "GBA", "N64", "SNES", "MKT", "RMX", "DX", "GP"], + "tags_cups": [ + "Switch", + "3DS", + "DS", + "GCN", + "GBA", + "N64", + "SNES", + "MKT", + "RMX", + "DX", + "GP" + ], "track_file_template": "{{ getattr(track, 'sha1', '_') }}", "multiplayer_disable_if": "getattr(track, 'warning', 0) >= 1", diff --git a/source/mkw/ModConfig.py b/source/mkw/ModConfig.py index aa5d25c..7779ee8 100644 --- a/source/mkw/ModConfig.py +++ b/source/mkw/ModConfig.py @@ -9,7 +9,7 @@ from source.mkw import Tag from source.mkw.Cup import Cup from source.mkw.MKWColor import bmg_color_text, bmg_color_raw from source.mkw.ModSettings import AbstractModSettings -from source.mkw.Track import Track +from source.mkw.Track.CustomTrack import CustomTrack import json from source.mkw.OriginalTrack import OriginalTrack @@ -68,20 +68,20 @@ class ModConfig: Representation of a mod """ - __slots__ = ("name", "path", "nickname", "variant", "tags_prefix", "tags_suffix", - "default_track", "_tracks", "version", "original_track_prefix", "swap_original_order", - "keep_original_track", "enable_random_cup", "tags_cups", "track_file_template", + __slots__ = ("name", "path", "nickname", "variant", "default_track", "_tracks", "version", + "original_track_prefix", "swap_original_order", "keep_original_track", + "enable_random_cup", "tags_cups", "track_file_template", "multiplayer_disable_if", "macros", "messages", "global_settings", - "specific_settings", "lpar_template") + "specific_settings", "lpar_template", "tags_template") def __init__(self, path: Path | str, name: str, nickname: str = None, version: str = None, variant: str = None, - tags_prefix: dict[Tag, str] = None, tags_suffix: dict[Tag, str] = None, tags_cups: list[Tag] = None, default_track: "Track | TrackGroup" = None, tracks: list["Track | TrackGroup"] = None, original_track_prefix: bool = None, swap_original_order: bool = None, keep_original_track: bool = None, enable_random_cup: bool = None, track_file_template: str = None, multiplayer_disable_if: str = None, macros: dict[str, str] = None, messages: dict[str, dict[str, str]] = None, global_settings: dict[str, dict[str, str]] = None, - specific_settings: dict[str, dict[str, str]] = None, lpar_template: str = None): + specific_settings: dict[str, dict[str, str]] = None, lpar_template: str = None, + tags_template: dict[str, str] = None): self.path = Path(path) self.macros: dict = macros if macros is not None else {} @@ -100,8 +100,7 @@ class ModConfig: self.version: str = version if version is not None else "v1.0.0" self.variant: str = variant if variant is not None else "01" - self.tags_prefix: dict[Tag] = tags_prefix if tags_prefix is not None else {} - self.tags_suffix: dict[Tag] = tags_suffix if tags_suffix is not None else {} + self.tags_template: dict[str, str] = tags_template if tags_template is not None else {} self.tags_cups: list[Tag] = tags_cups if tags_cups is not None else [] self.default_track: "Track | TrackGroup" = default_track if default_track is not None else None @@ -145,8 +144,8 @@ class ModConfig: **kwargs, - default_track=Track.from_dict(config_dict.get("default_track", {})), - tracks=[Track.from_dict(track) for track in config_dict.get("tracks", [])], + default_track=CustomTrack.from_dict(config_dict.get("default_track", {})), + tracks=[CustomTrack.from_dict(track) for track in config_dict.get("tracks", [])], macros=macros, messages=messages, ) @@ -204,7 +203,7 @@ class ModConfig: """ return self.path.parent - def get_all_tracks(self, *args, **kwargs) -> Generator["Track", None, None]: + def get_all_tracks(self, *args, **kwargs) -> Generator["CustomTrack", None, None]: """ Same as get_tracks, but track group are divided into subtracks """ @@ -425,7 +424,7 @@ class ModConfig: ) @threaded - def normalize_track(track: Track, track_file: Path): + def normalize_track(track: CustomTrack, track_file: Path): SZSPath(track_file).normalize( autoadd_path, destination_path / f"{track_file.stem}.szs", diff --git a/source/mkw/Patch/PatchOperation/BmgTxtEditor/FormatOriginalTrackLayer.py b/source/mkw/Patch/PatchOperation/BmgTxtEditor/FormatOriginalTrackLayer.py index 4b6b528..2630ddb 100644 --- a/source/mkw/Patch/PatchOperation/BmgTxtEditor/FormatOriginalTrackLayer.py +++ b/source/mkw/Patch/PatchOperation/BmgTxtEditor/FormatOriginalTrackLayer.py @@ -1,7 +1,7 @@ import re from source.mkw.Patch.PatchOperation.BmgTxtEditor import AbstractLayer -from source.mkw.Track import Track +from source.mkw.Track.CustomTrack import CustomTrack from source.wt import bmg @@ -46,7 +46,7 @@ class FormatOriginalTrackLayer(AbstractLayer): break else: tag = "Wii" - patched_name = Track( + patched_name = CustomTrack( name=name, tags=[tag] ).repr_format( diff --git a/source/mkw/Track.py b/source/mkw/Track.py deleted file mode 100644 index 23233bd..0000000 --- a/source/mkw/Track.py +++ /dev/null @@ -1,121 +0,0 @@ -from typing import Generator - -from source.mkw import Tag, Slot - -ModConfig: any - - -# representation of a custom track -class Track: - def __init__(self, special: Slot = None, music: Slot = None, tags: list[Tag] = None, weight: int = None, **kwargs): - self.special: Slot = special if special is not None else "T11" - self.music: Slot = music if music is not None else "T11" - self.tags: list[Tag] = tags if tags is not None else [] - self.weight: int = weight if weight is not None else 1 - - # others not mandatory attributes - for key, value in kwargs.items(): - # if the attribute start with __, this is a magic attribute, and it should not be modified - if key.startswith("__"): continue - setattr(self, key, value) - - def __repr__(self): - return f"" - - @classmethod - def from_dict(cls, track_dict: dict) -> "Track | TrackGroup": - """ - create a track from a dict, or create a track group is it is a group - :param track_dict: dict containing the track information - :return: Track - """ - if "group" in track_dict: - from source.mkw.TrackGroup import TrackGroup - return TrackGroup.from_dict(track_dict) - return cls(**track_dict) - - def get_tracks(self) -> Generator["Track", None, None]: - """ - Get all the track elements - :return: track elements - """ - for _ in range(self.weight): - yield self - - def repr_format(self, mod_config: "ModConfig", template: str) -> str: - """ - return the representation of the track from the format - :param template: template for the way the text will be represented - :param mod_config: configuration of the mod - :return: formatted representation of the track - """ - - return mod_config.multiple_safe_eval( - template, - env={ - "track": self, - "prefix": self.get_prefix(mod_config, ""), - "suffix": self.get_suffix(mod_config, "") - }, - ) - - def get_tag_template(self, mod_config: "ModConfig", templates: dict[str, str], default: any = None) -> any: - """ - Return the tag template found in templates. If not found, return default - :param mod_config: mod configuration - :param templates: template with all the tags and its replacement - :param default: default value if no tag template is found - :return: formatted representation of the tag - """ - for tag in filter(lambda tag: tag in templates, self.tags): - template: str = templates[tag] - return mod_config.multiple_safe_eval(template, env={"tag": tag}) - return default - - def get_prefix(self, mod_config: "ModConfig", default: any = None) -> any: - """ - return the prefix of the track - :param default: default value if no prefix is found - :param mod_config: mod configuration - :return: formatted representation of the track prefix - """ - return self.get_tag_template(mod_config, mod_config.tags_prefix, default) - - def get_suffix(self, mod_config: "ModConfig", default: any = None) -> any: - """ - return the suffix of the track - :param default: default value if no suffix is found - :param mod_config: mod configuration - :return: formatted representation of the track suffix - """ - return self.get_tag_template(mod_config, mod_config.tags_suffix, default) - - def is_new(self, mod_config: "ModConfig") -> bool: - """ - Return if the track should be considered as new for random selection - :param mod_config: mod configuration - :return: is the track new - """ - - return mod_config.safe_eval( - mod_config.global_settings["replace_random_new"].value, - env={"track": self} - ) is True - - def get_ctfile(self, mod_config: "ModConfig", template: str, hidden: bool = False) -> str: - """ - return the ctfile of the track - :hidden: if the track is in a group - :template: format of the track's name - :return: ctfile - """ - name = repr(self.repr_format(mod_config=mod_config, template=template)) - file_name = repr(self.repr_format(mod_config=mod_config, template=mod_config.track_file_template)) - - return ( - f'{"H" if hidden else "T"} {self.music}; ' # track type - f'{self.special}; {(0x04 if hidden else 0) | (0x01 if self.is_new(mod_config) else 0):#04x}; ' # lecode flags - f'{file_name}; ' # filename - f'{name}; ' # name of the track in the menu - f'{file_name}\n' # unique identifier for each track - ) diff --git a/source/mkw/Track/AbstractTrack.py b/source/mkw/Track/AbstractTrack.py new file mode 100644 index 0000000..28a5366 --- /dev/null +++ b/source/mkw/Track/AbstractTrack.py @@ -0,0 +1,103 @@ +from abc import ABC, abstractmethod +from typing import Generator + +from source.mkw import Slot, Tag, ModConfig + + +class TrackForbiddenCustomAttribute(Exception): + def __init__(self, attribute_name: str): + super().__init__(f"Forbidden track attribute : {attribute_name!r}") + + +class AbstractTrack(ABC): + music: Slot + special: Slot + tags: list[Tag] + weight: int + + def __init__(self, music: Slot = "T11", special: Slot = "T11", tags: list[Tag] = None, weight: int = 1, **kwargs): + self.music = music + self.special = special + self.tags = tags if tags is not None else [] + self.weight = weight + + # others not mandatory attributes + for key, value in kwargs.items(): + # if the attribute start with __, this is a magic attribute, and it should not be modified + if "__" in key or hasattr(self, key): raise TrackForbiddenCustomAttribute(key) + setattr(self, key, value) + + def __repr__(self): + return f"<{self.__class__.__name__} {id(self)}>" + + def get_tracks(self) -> Generator["AbstractTrack", None, None]: + """ + Return all the track itself or the subtracks if available + :return: all the track itself or the subtracks if available + """ + for _ in range(self.weight): + yield self + + def get_tag_template(self, mod_config: "ModConfig", template_name: str, default: any = None) -> any: + """ + Return the tag template found in templates. If not found, return default + :param mod_config: mod configuration + :param template_name: name of the template of the tags + :param default: default value if no tag template is found + :return: formatted representation of the tag + """ + for tag in filter(lambda tag: tag in mod_config.tags_template[template_name], self.tags): + return mod_config.multiple_safe_eval(mod_config.tags_template[template_name][tag], env={"tag": tag}) + return default + + def repr_format(self, mod_config: "ModConfig", template: str) -> str: + """ + return the representation of the track from the format + :param template: template for the way the text will be represented + :param mod_config: configuration of the mod + :return: formatted representation of the track + """ + return mod_config.multiple_safe_eval(template, env={"track": self, "get_tag_template": self.get_tag_template}) + + @abstractmethod + def get_filename(self, mod_config: "ModConfig") -> str: + """ + Return the filename of the track + :param mod_config: the mod_config object + :return: the filename of the track + """ + ... + + @abstractmethod + def is_new(self, mod_config: "ModConfig") -> bool: + """ + Return if the track should be considered as new for random selection + :param mod_config: mod configuration + :return: is the track new + """ + ... + + def get_ctfile(self, mod_config: "ModConfig", template: str, hidden: bool = False) -> str: + """ + return the ctfile of the track + :hidden: if the track is in a group + :template: format of the track's name + :return: ctfile + """ + category: str = "H" if hidden else "T" + name: str = self.repr_format(mod_config=mod_config, template=template) + filename: str = self.get_filename(mod_config=mod_config) + flags: int = ( + (0x04 if hidden else 0) | + (0x01 if self.is_new(mod_config) else 0) + ) + + return ( + f'{category} ' # category (is the track hidden, visible, an arena, ...) + f'{self.music}; ' # music + f'{self.special}; ' # property of the tracks + f'{flags:#04x}; ' # lecode flags + f'{filename!r}; ' # filename + f'{name!r}; ' # name of the track in the menu + f'{filename!r}\n' # unique identifier for each track + ) \ No newline at end of file diff --git a/source/mkw/Track/CustomTrack.py b/source/mkw/Track/CustomTrack.py new file mode 100644 index 0000000..6784808 --- /dev/null +++ b/source/mkw/Track/CustomTrack.py @@ -0,0 +1,35 @@ +from source.mkw.Track.AbstractTrack import AbstractTrack + +ModConfig: any + + +class CustomTrack(AbstractTrack): + """ + Represent a custom track + """ + + def __repr__(self): + return f"<{self.__class__.__name__} name={getattr(self, 'name', '/')} tags={getattr(self, 'tags', '/')}>" + + @classmethod + def from_dict(cls, track_dict: dict) -> "Track | TrackGroup": + """ + create a track from a dict, or create a track group is it is a group + :param track_dict: dict containing the track information + :return: Track + """ + if "group" in track_dict: + from source.mkw.TrackGroup import TrackGroup + return TrackGroup.from_dict(track_dict) + return cls(**track_dict) + + def get_filename(self, mod_config: "ModConfig") -> str: + return self.repr_format(mod_config=mod_config, template=mod_config.track_file_template) + + def is_new(self, mod_config: "ModConfig") -> bool: + """ + Return if the track should be considered as new for random selection + :param mod_config: mod configuration + :return: is the track new + """ + return mod_config.safe_eval(mod_config.global_settings["replace_random_new"].value, env={"track": self}) is True diff --git a/source/mkw/Track/DefaultTrack.py b/source/mkw/Track/DefaultTrack.py new file mode 100644 index 0000000..a503604 --- /dev/null +++ b/source/mkw/Track/DefaultTrack.py @@ -0,0 +1,9 @@ +from source.mkw.Track.AbstractTrack import AbstractTrack + + +class DefaultTrack(AbstractTrack): + def get_filename(self, mod_config: "ModConfig") -> str: + return "beginner_course" + + def is_new(self, mod_config: "ModConfig") -> bool: + return False diff --git a/source/mkw/Track/__init__.py b/source/mkw/Track/__init__.py new file mode 100644 index 0000000..3e54be3 --- /dev/null +++ b/source/mkw/Track/__init__.py @@ -0,0 +1 @@ +from source.mkw.Track import CustomTrack, DefaultTrack, AbstractTrack diff --git a/source/mkw/TrackGroup.py b/source/mkw/TrackGroup.py index 49fd24a..a4c9ceb 100644 --- a/source/mkw/TrackGroup.py +++ b/source/mkw/TrackGroup.py @@ -28,11 +28,11 @@ class TrackGroup: :param group_dict: dict containing the track information :return: TrackGroup or Track """ - from source.mkw.Track import Track + from source.mkw.Track.CustomTrack import CustomTrack - if "group" not in group_dict: return Track.from_dict(group_dict) + if "group" not in group_dict: return CustomTrack.from_dict(group_dict) return cls( - tracks=[Track.from_dict(track) for track in group_dict["group"]], + tracks=[CustomTrack.from_dict(track) for track in group_dict["group"]], tags=group_dict.get("tags"), name=group_dict.get("name"), )