diff --git a/Pack/MKWFaraphel/macros.json b/Pack/MKWFaraphel/macros.json index 3ec09e6..96c0e6a 100644 --- a/Pack/MKWFaraphel/macros.json +++ b/Pack/MKWFaraphel/macros.json @@ -1,17 +1,17 @@ { "GAME_REGION": "str(extracted_game.original_game.wit_path.region)", - "TRACK_TEXT_SCORE": "get_color(name=['yellow', 'orange', 'dark red', 'azure'][getattr(track, 'warning', 0)]).color_text(f'\\\\x{65296+track.score:x} ') if hasattr(track, 'score') else ''", - "TRACK_TEXT_PREFIX": "f'{prefix} ' if (prefix := get_tag_template('prefix', '')) else ''", - "TRACK_TEXT_SUFFIX": "f' ({suffix})' if (suffix := get_tag_template('suffix', '')) else ''", - "TRACK_TEXT_NAME": "getattr(track, 'name', '')", - "TRACK_TEXT_AUTHORS": "'\\\\n'.join(author) if isinstance(author := getattr(track, 'author', '/'), list) else author", - "TRACK_TEXT_WARNING_IF_DISABLED": "get_color(name='red').color_text('/') if getattr(track, 'warning', 0) != 0 else ''", + "TRACK_TEXT_SCORE": "get_color(name=['yellow', 'orange', 'dark red', 'azure'][track.warning]).color_text(f'\\\\x{65296+track.score:x} ') if track.score else ''", + "TRACK_TEXT_PREFIX": "f'{prefix} ' if (prefix := track.get_tag_template('prefix', '')) else ''", + "TRACK_TEXT_SUFFIX": "f' ({suffix})' if (suffix := track.get_tag_template('suffix', '')) else ''", + "TRACK_TEXT_NAME": "track.name", + "TRACK_TEXT_AUTHORS": "'\\\\n'.join(track.author) if isinstance(track.author, list) else track.author", + "TRACK_TEXT_WARNING_IF_DISABLED": "get_color(name='red').color_text('/') if track.warning != 0 else ''", "TRACK_TEXT_HIGHLIGHT_START": "get_color(name='azure').raw if eval(highlight_if if (highlight_if := mod_config.specific_settings['highlight_if'].value) is not None else 'False', env={'track': track, 'mod_config': mod_config}) is True else ''", "TRACK_TEXT_HIGHLIGHT_END": "get_color(name='off').raw", "SETTINGS_MODE": "mod_config.specific_settings['mode'].value", "SETTINGS_BALANCING": "mod_config.specific_settings['balancing'].value", - "IF_NO_WARNING": "if getattr(track, 'warning', 0) == 0 else ''" + "IF_NO_WARNING": "if track.warning == 0 else ''" } \ No newline at end of file diff --git a/Pack/MKWFaraphel/mod_config.json b/Pack/MKWFaraphel/mod_config.json index 1a3c779..cfbb835 100644 --- a/Pack/MKWFaraphel/mod_config.json +++ b/Pack/MKWFaraphel/mod_config.json @@ -18,7 +18,7 @@ } }, "replace_random_new": { - "default": "'Retro' not in getattr(track, 'tags', []) and getattr(track, 'warning', 0) == 0" + "default": "'Retro' not in track.tags and track.warning == 0" } }, "specific_settings": { @@ -39,7 +39,7 @@ "fr": "Surligner si" }, "type": "string", - "default": "'v' + getattr(track, 'since_version', '') == mod_config.version", + "default": "'v' + track.since_version == mod_config.version", "preview": "track_selecting" }, "balancing": { @@ -124,9 +124,18 @@ "GP" ], - "track_file_template": "{{ getattr(track, 'sha1', '_') }}", - "multiplayer_disable_if": "getattr(track, 'warning', 0) >= 1", + "track_file_template": "{{ track.sha1 }}", + "multiplayer_disable_if": "track.warning >= 1", + "default_track_attributes": { + "since_version": "", + "sha1": "_", + "name": "", + "author": "/", + "warning": 0, + "score": 0, + "tags": [] + }, "tracks": [ { "name":"Mario Kart Stadium", diff --git a/source/gui/preview/track_formatting.py b/source/gui/preview/track_formatting.py index f147105..a9096c7 100644 --- a/source/gui/preview/track_formatting.py +++ b/source/gui/preview/track_formatting.py @@ -52,9 +52,7 @@ class Window(AbstractPreviewWindow): # insert all the tracks representation for track in self.mod_config.get_all_arenas_tracks(ignore_filter=True): try: - track_repr = track.repr_format( - self.mod_config, self.entry_template_input.get() - ) + track_repr = track.repr_format(template=self.entry_template_input.get()) offset: int = 0 # the color tag is removed at every sub, so keep track of the offset tags: list[tuple[int | None, str | None]] = [] # list of all the position of the tags, with the offset diff --git a/source/gui/preview/track_selecting.py b/source/gui/preview/track_selecting.py index 9de42c3..db5564e 100644 --- a/source/gui/preview/track_selecting.py +++ b/source/gui/preview/track_selecting.py @@ -80,7 +80,7 @@ class Window(AbstractPreviewWindow): self.text_track_select.configure(state=tkinter.NORMAL) self.text_track_select.delete(1.0, tkinter.END) - template_func = self.mod_config.safe_eval(self.entry_template_input.get(), args=["track"]) + template_func = self.mod_config.safe_eval(template=self.entry_template_input.get(), args=["track"]) for track in self.mod_config.get_all_tracks(ignore_filter=True): value: bool = template_func(track) is True diff --git a/source/mkw/ModConfig.py b/source/mkw/ModConfig.py index d0112c8..205baaf 100644 --- a/source/mkw/ModConfig.py +++ b/source/mkw/ModConfig.py @@ -7,12 +7,13 @@ from PIL import Image from source import threaded from source.mkw import Tag -from source.mkw.Cup import Cup +from source.mkw.Track.Cup import Cup from source.mkw.collection import MKWColor, Slot from source.mkw.ModSettings import AbstractModSettings from source.mkw.Track import CustomTrack, DefaultTrack, Arena from source.progress import Progress -from source.safe_eval import safe_eval, multiple_safe_eval +from source.safe_eval.safe_eval import safe_eval +from source.safe_eval.multiple_safe_eval import multiple_safe_eval from source.wt.szs import SZSPath from source.translation import translate as _ @@ -78,8 +79,9 @@ class ModConfig: version: str = "v1.0.0" variant: str = "01" - _tracks: list["Track | TrackGroup"] = field(default_factory=list) - _arenas: list["Arena"] = field(default_factory=list) + _tracks: list["Track | TrackGroup | dict"] = field(default_factory=list) + default_track_attributes: dict[str, any] = field(default_factory=dict) + _arenas: list["Arena | dict"] = field(default_factory=list) track_file_template: "TemplateMultipleSafeEval" = "{{ getattr(track, 'sha1', '_') }}" multiplayer_disable_if: "TemplateSafeEval" = "False" @@ -94,8 +96,8 @@ class ModConfig: macros: dict[str, "TemplateSafeEval"] = field(default_factory=dict) messages: dict[str, dict[str, "TemplateMultipleSafeEval"]] = field(default_factory=dict) - global_settings: dict[str, "AbstractModSettings"] = field(default_factory=dict) - specific_settings: dict[str, "AbstractModSettings"] = field(default_factory=dict) + global_settings: dict[str, "AbstractModSettings | dict"] = field(default_factory=dict) + specific_settings: dict[str, "AbstractModSettings | dict"] = field(default_factory=dict) lpar_template: "TemplateMultipleSafeEval" = "normal.lpar" @@ -103,6 +105,15 @@ class ModConfig: self.path = Path(self.path) if self.nickname is None: self.nickname = self.name + self._tracks = [CustomTrack.from_dict(self, track) for track in self._tracks if isinstance(track, dict)] + self._arenas = [Arena.from_dict(self, arena) for arena in self._arenas if isinstance(arena, dict)] + + self.global_settings = {name: AbstractModSettings.get(data) for name, data in merge_dict( + default_global_settings, self.global_settings, dict_keys=default_global_settings.keys() + # Avoid modder to add their own settings to globals one + ).items()} + self.specific_settings = {name: AbstractModSettings.get(data) for name, data in self.specific_settings.items()} + def __hash__(self) -> int: return hash(self.name) @@ -125,21 +136,11 @@ class ModConfig: kwargs = config_dict | { "path": path, - "_tracks": [CustomTrack.from_dict(track) for track in config_dict.pop("tracks", [])], - "_arenas": [Arena.from_dict(arena) for arena in config_dict.pop("arenas", [])], + "_tracks": config_dict.pop("tracks", []), + "_arenas": config_dict.pop("arenas", []), "macros": macros, "messages": messages, - - "global_settings": {name: AbstractModSettings.get(data) for name, data in merge_dict( - default_global_settings, - config_dict.get("global_settings", {}), - dict_keys=default_global_settings.keys() # Avoid modder to add their own settings to globals one - ).items()}, - - "specific_settings": {name: AbstractModSettings.get(data) for name, data in config_dict.get( - "specific_settings", {} - ).items()}, } return cls(**kwargs) @@ -233,7 +234,7 @@ class ModConfig: # filter_template_func is the function checking if the track should be included. If no parameter is set, # then always return True filter_template_func: Callable = self.safe_eval( - filter_template if filter_template is not None else "True", args=["track"] + template=filter_template if filter_template is not None else "True", args=["track"] ) # if a sorting function is set, use it. If no function is set, but sorting is not disabled, use settings. @@ -247,7 +248,7 @@ class ModConfig: # wrap the iterator inside a sort function iterator = sorted(iterator, key=sorting_template_func) - # Go though all the tracks and filter them if enabled + # Go through all the tracks and filter them if enabled for track in iterator: yield track @@ -267,13 +268,13 @@ class ModConfig: if len(track_buffer) > 4: current_tag_count += 1 - yield Cup(tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count}") + yield Cup(mod_config=self, 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([DefaultTrack()] * (4 - len(track_buffer))) - yield Cup(tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count + 1}") + track_buffer.extend([DefaultTrack(mod_config=self)] * (4 - len(track_buffer))) + yield Cup(mod_config=self, tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count + 1}") def get_unordered_cups(self) -> Generator["Cup", None, None]: """ @@ -289,13 +290,13 @@ class ModConfig: track_buffer.append(track) if len(track_buffer) > 4: - yield Cup(tracks=track_buffer) + yield Cup(mod_config=self, tracks=track_buffer) 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([DefaultTrack()] * (4 - len(track_buffer))) - yield Cup(tracks=track_buffer) + track_buffer.extend([DefaultTrack(mod_config=self)] * (4 - len(track_buffer))) + yield Cup(mod_config=self, tracks=track_buffer) def get_cups(self) -> Generator["Cup", None, None]: """ @@ -328,11 +329,11 @@ class ModConfig: for cup in self.get_cups(): # get all the cup ctfile, use "-" for the template since the track's name are not used here - ctfile += cup.get_ctfile(mod_config=self, template=template) + ctfile += cup.get_ctfile(template=template) ctfile_override_property = "[SETUP-ARENA]\n" for arena in self.get_arenas(): - arena_definition, arena_override_property = arena.get_ctfile(mod_config=self, template=template) + arena_definition, arena_override_property = arena.get_ctfile(template=template) ctfile += arena_definition ctfile_override_property += arena_override_property @@ -433,7 +434,7 @@ class ModConfig: for track in self.get_all_arenas_tracks(): track_file: Path = next( - track_directory.rglob(f"{track.repr_format(self, self.track_file_template)}.*") + track_directory.rglob(f"{track.repr_format(template=self.track_file_template)}.*") ) @threaded diff --git a/source/mkw/Patch/PatchOperation/BmgTxtEditor/FormatOriginalTrackLayer.py b/source/mkw/Patch/PatchOperation/BmgTxtEditor/FormatOriginalTrackLayer.py index 46389f1..7806da4 100644 --- a/source/mkw/Patch/PatchOperation/BmgTxtEditor/FormatOriginalTrackLayer.py +++ b/source/mkw/Patch/PatchOperation/BmgTxtEditor/FormatOriginalTrackLayer.py @@ -49,11 +49,11 @@ class FormatOriginalTrackLayer(AbstractLayer): else: tag = "Wii" patched_name = CustomTrack( + mod_config=patch.mod_config, name=name, tags=[tag] ).repr_format( - patch.mod_config, - self.template + template=self.template ) return f" {id}\t= {patched_name}" diff --git a/source/mkw/Track/AbstractTrack.py b/source/mkw/Track/AbstractTrack.py index d76b326..230f205 100644 --- a/source/mkw/Track/AbstractTrack.py +++ b/source/mkw/Track/AbstractTrack.py @@ -15,13 +15,17 @@ class TrackForbiddenCustomAttribute(Exception): class AbstractTrack(ABC): + + mod_config: "ModConfig" music: Slot.Slot special: Slot.Slot tags: list[Tag] weight: int - def __init__(self, music: Slot.Slot = "T11", special: Slot.Slot = "T11", + def __init__(self, mod_config: "ModConfig", music: Slot.Slot = "T11", special: Slot.Slot = "T11", tags: list[Tag] = None, weight: int = 1, **kwargs): + + self.mod_config = mod_config self.music = Slot.find(music) self.special = Slot.find(special) self.tags = tags if tags is not None else [] @@ -30,7 +34,8 @@ class AbstractTrack(ABC): # 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) + if "__" in key: raise TrackForbiddenCustomAttribute(key) + # TODO: check potential security issue with setattr and already implemented method and attribute setattr(self, key, value) def __repr__(self): @@ -45,34 +50,33 @@ class AbstractTrack(ABC): yield self @abstractmethod - def repr_format(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> str: + def repr_format(self, template: "TemplateMultipleSafeEval") -> 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 """ ... + @property @abstractmethod - def get_filename(self, mod_config: "ModConfig") -> str: + def filename(self) -> str: """ Return the filename of the track - :param mod_config: the mod_config object :return: the filename of the track """ ... + @property @abstractmethod - def is_new(self, mod_config: "ModConfig") -> bool: + def is_new(self) -> 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: "TemplateMultipleSafeEval", hidden: bool = False) -> str: + def get_ctfile(self, template: "TemplateMultipleSafeEval", hidden: bool = False) -> str: """ return the ctfile of the track :hidden: if the track is in a group @@ -80,11 +84,11 @@ class AbstractTrack(ABC): :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) + name: str = self.repr_format(template=template) + filename: str = self.filename flags: int = ( (0x04 if hidden else 0) | - (0x01 if self.is_new(mod_config) else 0) + (0x01 if self.is_new else 0) ) return ( diff --git a/source/mkw/Track/Arena.py b/source/mkw/Track/Arena.py index 6100321..4e83daf 100644 --- a/source/mkw/Track/Arena.py +++ b/source/mkw/Track/Arena.py @@ -16,13 +16,20 @@ class ArenaForbiddenCustomAttribute(Exception): class Arena(RealArenaTrack): + """ + Represent an arena object + """ + + mod_config: "ModConfig" slot: Slot.Slot music: Slot.Slot special: Slot.Slot tags: list[Tag] - def __init__(self, slot: Slot.Slot, music: Slot.Slot = None, special: Slot.Slot = None, + def __init__(self, mod_config: "ModConfig", slot: Slot.Slot, music: Slot.Slot = None, special: Slot.Slot = None, tags: list[Tag] = None, **kwargs): + + self.mod_config = mod_config self.slot = Slot.find(slot) self.music = Slot.find(music if music is not None else slot) self.special = Slot.find(special if special is not None else slot) @@ -31,26 +38,25 @@ class Arena(RealArenaTrack): # 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 ArenaForbiddenCustomAttribute(key) + if "__" in key: raise ArenaForbiddenCustomAttribute(key) setattr(self, key, value) def __repr__(self): return f"<{self.__class__.__name__} name={getattr(self, 'name', '/')} tags={getattr(self, 'tags', '/')}>" @classmethod - def from_dict(cls, arena_dict: dict[str, any]) -> "Arena": - return cls(**arena_dict) + def from_dict(cls, mod_config: "ModConfig", arena_dict: dict[str, any]) -> "Arena": + return cls(mod_config, **arena_dict) - def get_ctfile(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> (str, str): + def get_ctfile(self, template: "TemplateMultipleSafeEval") -> (str, str): """ Return the ctfile for the arena and the redefinition of the slot property - :param mod_config: the mod_config object :param template: the template of the track name :return: the ctfile for the arena and the redefinition of the slot property """ - name: str = self.repr_format(mod_config=mod_config, template=template) - filename: str = self.get_filename(mod_config=mod_config) + name: str = self.repr_format(template=template) + filename: str = self.filename return ( ( diff --git a/source/mkw/Cup.py b/source/mkw/Track/Cup.py similarity index 76% rename from source/mkw/Cup.py rename to source/mkw/Track/Cup.py index 48bd608..4e5c0a4 100644 --- a/source/mkw/Cup.py +++ b/source/mkw/Track/Cup.py @@ -12,10 +12,12 @@ class Cup: class that represent a mario kart wii track cup """ - __slots__ = ["_tracks", "cup_name", "cup_id"] + __slots__ = ["_tracks", "cup_name", "cup_id", "mod_config"] _last_cup_id = 0 + mod_config: "ModConfig" - def __init__(self, tracks: list["Track | TrackGroup"], cup_name: str | None = None): + def __init__(self, mod_config: "ModConfig", tracks: list["Track | TrackGroup"], cup_name: str | None = None): + self.mod_config = mod_config self._tracks = tracks[:4] self.cup_id = self.__class__._last_cup_id @@ -25,7 +27,7 @@ class Cup: def __repr__(self): return f"" - def get_default_cticon(self, mod_config: "ModConfig") -> Image.Image: + def get_default_cticon(self) -> Image.Image: """ Get the default cticon for this cup :return: the default cticon @@ -33,7 +35,7 @@ class Cup: from source.mkw.ModConfig import CT_ICON_SIZE ct_icon = Image.new("RGBA", (CT_ICON_SIZE, CT_ICON_SIZE)) - default_font_path = str(mod_config.get_default_font().resolve()) + default_font_path = str(self.mod_config.get_default_font().resolve()) draw = ImageDraw.Draw(ct_icon) draw.text( @@ -63,15 +65,15 @@ class Cup: 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) + return self.get_default_cticon() - def get_ctfile(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> str: + def get_ctfile(self, template: "TemplateMultipleSafeEval") -> str: """ Get the ctfile for this cup :return: the ctfile """ ctfile = f'C "{self.cup_name}"\n' - for track in self._tracks: ctfile += track.get_ctfile(mod_config, template) + for track in self._tracks: ctfile += track.get_ctfile(template=template) ctfile += "\n" return ctfile diff --git a/source/mkw/Track/CustomTrack.py b/source/mkw/Track/CustomTrack.py index b655e90..e4f1fa8 100644 --- a/source/mkw/Track/CustomTrack.py +++ b/source/mkw/Track/CustomTrack.py @@ -15,19 +15,21 @@ class CustomTrack(RealArenaTrack, AbstractTrack): return f"<{self.__class__.__name__} name={getattr(self, 'name', '/')} tags={getattr(self, 'tags', '/')}>" @classmethod - def from_dict(cls, track_dict: dict) -> "Track | TrackGroup": + def from_dict(cls, mod_config: "ModConfig", track_dict: dict) -> "Track | TrackGroup": """ create a track from a dict, or create a track group is it is a group + :param mod_config: the mod configuration :param track_dict: dict containing the track information :return: Track """ if "group" in track_dict: from source.mkw.Track.TrackGroup import TrackGroup - return TrackGroup.from_dict(track_dict) - return cls(**track_dict) + return TrackGroup.from_dict(mod_config, track_dict) + return cls(mod_config, **track_dict) - def is_new(self, mod_config: "ModConfig") -> bool: - return mod_config.safe_eval( - mod_config.global_settings["replace_random_new"].value, + @property + def is_new(self) -> bool: + return self.mod_config.safe_eval( + self.mod_config.global_settings["replace_random_new"].value, args=["track"] )(track=self) is True diff --git a/source/mkw/Track/DefaultTrack.py b/source/mkw/Track/DefaultTrack.py index 52f6889..b46231d 100644 --- a/source/mkw/Track/DefaultTrack.py +++ b/source/mkw/Track/DefaultTrack.py @@ -7,11 +7,13 @@ if TYPE_CHECKING: class DefaultTrack(AbstractTrack): - def repr_format(self, mod_config: "ModConfig", template: str) -> str: + def repr_format(self, template: str) -> str: return " " # the name is always a blank space. Using nothing result in the filename being used instead - def get_filename(self, mod_config: "ModConfig") -> str: + @property + def filename(self) -> str: return "beginner_course" # by default, use the T11 track, beginner_course - def is_new(self, mod_config: "ModConfig") -> bool: + @property + def is_new(self) -> bool: return False # default track are never selected for random cup diff --git a/source/mkw/Track/RealArenaTrack.py b/source/mkw/Track/RealArenaTrack.py index 495a735..069e977 100644 --- a/source/mkw/Track/RealArenaTrack.py +++ b/source/mkw/Track/RealArenaTrack.py @@ -12,32 +12,29 @@ class RealArenaTrack: (For example, DefaultTrack is not considered a real track class) """ + mod_config: "ModConfig" tags: list["Tag"] - def get_tag_template(self, mod_config: "ModConfig", template_name: str, default: any = None) -> any: + def get_tag_template(self, 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_templates[template_name], self.tags): - return mod_config.multiple_safe_eval( - mod_config.tags_templates[template_name][tag], + for tag in filter(lambda tag: tag in self.mod_config.tags_templates[template_name], self.tags): + return self.mod_config.multiple_safe_eval( + template=self.mod_config.tags_templates[template_name][tag], args=["tag"], )(tag=tag) return default - def repr_format(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> str: - return mod_config.multiple_safe_eval( - template, - args=["track", "get_tag_template"] - )( - track=self, - get_tag_template=lambda *args, **kwargs: self.get_tag_template(mod_config, *args, **kwargs) - # get_tag_template can't be in env because it is dependent of the track self - ) + def repr_format(self, template: "TemplateMultipleSafeEval") -> str: + return self.mod_config.multiple_safe_eval(template=template, args=["track"])(track=self) - def get_filename(self, mod_config: "ModConfig") -> str: - return self.repr_format(mod_config=mod_config, template=mod_config.track_file_template) + @property + def filename(self) -> str: + return self.repr_format(template=self.mod_config.track_file_template) + + def __getattr__(self, item): + return self.mod_config.default_track_attributes.get(item, None) diff --git a/source/mkw/Track/TrackGroup.py b/source/mkw/Track/TrackGroup.py index 1afe300..48f4f67 100644 --- a/source/mkw/Track/TrackGroup.py +++ b/source/mkw/Track/TrackGroup.py @@ -1,6 +1,7 @@ from typing import Generator, TYPE_CHECKING from source.mkw import Tag +from source.translation import translate as _ if TYPE_CHECKING: from source import TemplateMultipleSafeEval @@ -8,11 +9,29 @@ if TYPE_CHECKING: from source.mkw.ModConfig import ModConfig +class TrackGroupForbiddenCustomAttribute(Exception): + def __init__(self, attribute_name: str): + super().__init__(_("FORBIDDEN_TRACKGROUP_ATTRIBUTE", " : ", repr(attribute_name))) + + class TrackGroup: - def __init__(self, tracks: list["CustomTrack"] = None, tags: list[Tag] = None): + + mod_config: "ModConfig" + tracks: list["CustomTrack"] + tags: list["Tag"] + + def __init__(self, mod_config: "ModConfig", tracks: list["CustomTrack"] = None, + tags: list["Tag"] = None, **kwargs): + self.mod_config = mod_config self.tracks = tracks if tracks is not None else [] self.tags = tags if tags is not None else [] + # 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: raise TrackGroupForbiddenCustomAttribute(key) + setattr(self, key, value) + def get_tracks(self) -> Generator["CustomTrack", None, None]: """ Get all the track elements @@ -22,27 +41,28 @@ class TrackGroup: yield from track.get_tracks() @classmethod - def from_dict(cls, group_dict: dict) -> "TrackGroup | Track": + def from_dict(cls, mod_config: "ModConfig", group_dict: dict) -> "TrackGroup | Track": """ create a track group from a dict, or create a track from the dict if not a group + :param mod_config: the mod configuration :param group_dict: dict containing the track information :return: TrackGroup or Track """ from source.mkw.Track.CustomTrack import CustomTrack - if "group" not in group_dict: return CustomTrack.from_dict(group_dict) + if "group" not in group_dict: return CustomTrack.from_dict(mod_config, group_dict) return cls( - tracks=[CustomTrack.from_dict(track) for track in group_dict["group"]], - tags=group_dict.get("tags"), + tracks=[CustomTrack.from_dict(mod_config, track) for track in group_dict["group"]], + **group_dict, ) - def get_ctfile(self, mod_config: "ModConfig", template: "TemplateMultipleSafeEval") -> str: + def get_ctfile(self, template: "TemplateMultipleSafeEval") -> str: """ return the ctfile of the track group :return: ctfile """ ctfile = f'T T11; T11; 0x02; "-"; "info"; "-"\n' for track in self.get_tracks(): - ctfile += track.get_ctfile(template=template, mod_config=mod_config, hidden=True) + ctfile += track.get_ctfile(template=template, hidden=True) return ctfile