diff --git a/Pack/MKWFaraphel/mod_config.json b/Pack/MKWFaraphel/mod_config.json index feaf5ce..9f57e5d 100644 --- a/Pack/MKWFaraphel/mod_config.json +++ b/Pack/MKWFaraphel/mod_config.json @@ -1,5 +1,5 @@ { - "version": "0.11", + "version": "v0.12", "name": "Mario Kart Wii Faraphel", "nickname": "MKWF", "variant": "60", @@ -67,16 +67,17 @@ }, "tags_cups": ["Wii U", "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 '' }}", + "race_name": "{{ getattr(track, 'name', '/') }}", + "file_name": "{{ getattr(track, 'sha1', '/') }}" + }, + "default_track": { "music":"T32", "special":"T32", "author":"MrFluffy", - "since_version":"0.1", - "sha1":"54a1621fef2b137adcbe20b6dd710b5bc5f981a1", - "version":"v1.1", - "tags":[ - - ] + "tags":[] }, "tracks": [ { diff --git a/assets/language/en.json b/assets/language/en.json index 0173745..f496b03 100644 --- a/assets/language/en.json +++ b/assets/language/en.json @@ -3,7 +3,7 @@ "translation": { "INSTALLER_TITLE": "MKWF-Install", "LANGUAGE_SELECTION": "Language", - "TRACK_CONFIGURATION": "Track Configuration", + "TRACK_FILTER": "Track Filters", "ADVANCED_CONFIGURATION": "Advanced", "HELP": "Help" } diff --git a/assets/language/fr.json b/assets/language/fr.json index 0cc76ef..c06de2a 100644 --- a/assets/language/fr.json +++ b/assets/language/fr.json @@ -3,7 +3,7 @@ "translation": { "INSTALLER_TITLE": "MKWF-Install", "LANGUAGE_SELECTION": "Langue", - "TRACK_CONFIGURATION": "Configuration des courses", + "TRACK_FILTER": "filtrer les courses", "ADVANCED_CONFIGURATION": "Avancée", "HELP": "Aide" } diff --git a/source/gui/install.py b/source/gui/install.py index 8ca41c8..dc037bc 100644 --- a/source/gui/install.py +++ b/source/gui/install.py @@ -18,6 +18,8 @@ from source import event from source import * import os +from source.wt.wit import Extension + class SourceGameError(Exception): def __init__(self, path: Path | str): @@ -126,6 +128,13 @@ class Window(tkinter.Tk): """ return self.destination_game.get_path() + def get_output_type(self) -> Extension: + """ + Get the output type + :return: output type + """ + return self.destination_game.get_output_type() + # Menu bar class Menu(tkinter.Menu): @@ -159,8 +168,8 @@ class Menu(tkinter.Menu): def __init__(self, master: tkinter.Menu): super().__init__(master, tearoff=False) - master.add_cascade(label=_("TRACK_CONFIGURATION"), menu=self) - self.add_command(label="Change configuration") + master.add_cascade(label=_("TRACK_FILTER"), menu=self) + self.add_command(label="Change filter") # Advanced menu class Advanced(tkinter.Menu): @@ -209,6 +218,7 @@ class Menu(tkinter.Menu): class SourceGame(ttk.LabelFrame): def __init__(self, master: tkinter.Tk): super().__init__(master, text="Original Game File") + self.columnconfigure(1, weight=1) self.entry = ttk.Entry(self, width=50) self.entry.grid(row=1, column=1, sticky="nsew") @@ -268,12 +278,17 @@ class SourceGame(ttk.LabelFrame): class DestinationGame(ttk.LabelFrame): def __init__(self, master: tkinter.Tk): super().__init__(master, text="Game Directory Destination") + self.columnconfigure(1, weight=1) - self.entry = ttk.Entry(self, width=50) + self.entry = ttk.Entry(self) self.entry.grid(row=1, column=1, sticky="nsew") + self.output_type = ttk.Combobox(self, width=5, values=[extension.name for extension in Extension]) + self.output_type.set(Extension.WBFS.name) + self.output_type.grid(row=1, column=2, sticky="nsew") + self.button = ttk.Button(self, text="...", width=2, command=self.select) - self.button.grid(row=1, column=2, sticky="nsew") + self.button.grid(row=1, column=3, sticky="nsew") def select(self) -> None: """ @@ -304,6 +319,13 @@ class DestinationGame(ttk.LabelFrame): if not path.exists(): raise DestinationGameError(path) return path + def get_output_type(self) -> Extension: + """ + Get the output type + :return: the output type + """ + return Extension[self.output_type.get()] + def set_state(self, state: InstallerState) -> None: """ Set the progress bar state when the installer change state @@ -312,7 +334,9 @@ class DestinationGame(ttk.LabelFrame): """ for child in self.winfo_children(): match state: - case InstallerState.IDLE: child.config(state="normal") + case InstallerState.IDLE: + if child == self.output_type: child.config(state="readonly") + else: child.config(state="normal") case InstallerState.INSTALLING: child.config(state="disabled") @@ -339,14 +363,20 @@ class ButtonInstall(ttk.Button): messagebox.showerror(_("ERROR"), _("ERROR_INVALID_DESTINATION_GAME")) return - # get space remaining on the C: drive + # if there is no more space on the installer drive, show a warning if shutil.disk_usage(".").free < minimum_space_available: if not messagebox.askokcancel(_("WARNING"), _("WARNING_LOW_SPACE_CONTINUE")): return + # if there is no more space on the destination drive, show a warning + elif shutil.disk_usage(destination_path).free < minimum_space_available: + if not messagebox.askokcancel(_("WARNING"), _("WARNING_LOW_SPACE_CONTINUE")): + return + game = Game(source_path) mod_config = self.master.get_mod_config() - self.master.progress_function(game.install_mod(destination_path, mod_config)) + output_type = self.master.get_output_type() + self.master.progress_function(game.install_mod(destination_path, mod_config, output_type)) finally: self.master.set_state(InstallerState.IDLE) diff --git a/source/mkw/Cup.py b/source/mkw/Cup.py index 4820acc..f5c7b5e 100644 --- a/source/mkw/Cup.py +++ b/source/mkw/Cup.py @@ -1,4 +1,33 @@ # class that represent a mario kart wii cup +from PIL import Image + + class Cup: - def __init__(self, track1: "Track" = None, track2: "Track" = None, track3: "Track" = None, track4: "Track" = None): - self._tracks = [track1, track2, track3, track4] \ No newline at end of file + __slots__ = ["_tracks", "cup_id"] + _last_cup_id = 0 + + def __init__(self, tracks: list["Track | TrackGroup"], cup_id: 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 + + def __repr__(self): + return f"" + + def get_cup_icon(self) -> Image.Image: + ... + + def get_ctfile(self, mod_config: "ModConfig") -> str: + """ + Get the ctfile for this cup + :return: the ctfile + """ + ctfile = f'C "{self.cup_id}"\n' + for track in self._tracks: ctfile += track.get_ctfile(mod_config=mod_config) + ctfile += "\n" + + return ctfile diff --git a/source/mkw/Game.py b/source/mkw/Game.py index cf98298..5cddbad 100644 --- a/source/mkw/Game.py +++ b/source/mkw/Game.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Generator from source.mkw.ModConfig import ModConfig -from source.wt.wit import WITPath, Region +from source.wt.wit import WITPath, Region, Extension class Game: @@ -24,7 +24,7 @@ class Game: """ return not any(self.wit_path[f"./files/rel/lecode-{region.value}.bin"].exists() for region in Region) - def extract(self, dest: Path | str) -> Generator[str, None, Path]: + def extract(self, dest: Path | str) -> Generator[dict, None, Path]: """ Extract the game to the destination directory. If the game is a FST, just copy to the destination :param dest: destination directory @@ -43,10 +43,13 @@ class Game: except StopIteration as e: return e.value - def install_mod(self, dest: Path, mod_config: ModConfig) -> Generator[str, None, None]: + def install_mod(self, dest: Path, mod_config: ModConfig, output_type: Extension) -> Generator[dict, None, None]: """ Patch the game with the mod :dest: destination directory :mod_config: mod configuration + :output_type: type of the destination game """ - yield from self.extract(dest / f"{mod_config.nickname} {mod_config.version}") + # yield from self.extract(dest / f"{mod_config.nickname} {mod_config.version}") + print(mod_config.get_ctfile()) + yield {} diff --git a/source/mkw/ModConfig.py b/source/mkw/ModConfig.py index 1045db9..6f2fed3 100644 --- a/source/mkw/ModConfig.py +++ b/source/mkw/ModConfig.py @@ -2,6 +2,7 @@ from pathlib import Path from typing import Generator from source.mkw import Tag, Color +from source.mkw.Cup import Cup from source.mkw.Track import Track import json @@ -10,33 +11,42 @@ import json class ModConfig: __slots__ = ("name", "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") + "keep_original_track", "enable_random_cup", "tags_cups", "track_formatting") def __init__(self, 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, original_track_prefix: bool = None, swap_original_order: bool = None, - keep_original_track: bool = None, enable_random_cup: bool = None): + keep_original_track: bool = None, enable_random_cup: bool = None, + track_formatting: dict[str, str] = None): 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 "1.0.0" + 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.region: dict[int] | int = region if region is not None else 0 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_cups: dict[Tag] = tags_cups if tags_cups 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 self._tracks: list["Track | TrackGroup"] = tracks if tracks is not None else [] + self.track_formatting: dict[str, str] = { + "menu_name": "{{ getattr(track, 'name', '/') }}", + "race_name": "{{ getattr(track, 'name', '/') }}", + "file_name": "{{ getattr(track, 'sha1', '/') }}" + } | (track_formatting if track_formatting is not None else {}) self.original_track_prefix: bool = original_track_prefix if original_track_prefix is not None else True self.swap_original_order: bool = swap_original_order if swap_original_order is not None else True self.keep_original_track: bool = keep_original_track if keep_original_track is not None else True self.enable_random_cup: bool = enable_random_cup if enable_random_cup is not None else True + def __repr__(self): + return f"" + @classmethod def from_dict(cls, config_dict: dict) -> "ModConfig": """ @@ -44,11 +54,11 @@ class ModConfig: :param config_dict: dict containing the configuration :return: ModConfig """ - kwargs = { attr: config_dict.get(attr) - for attr in ["nickname", "version", "variant", "tags_prefix", "tags_suffix", "tags_cups", - "original_track_prefix", "swap_original_order", "keep_original_track", "enable_random_cup"] + for attr in cls.__slots__ + if attr not in ["name", "default_track", "_tracks", "tracks"] + # these keys are treated after or are reserved } return cls( @@ -78,3 +88,82 @@ class ModConfig: for track in self._tracks: yield from track.get_tracks() + def get_ordered_cups(self) -> Generator["Cup", None, None]: + """ + Get all the cups with cup tags + :return: cups with cup tags + """ + # use self._tracks instead of self._get_tracks() because we want the TrackGroup + # for track that have a tag in self.tags_cups + for tag_cup in self.tags_cups: + track_buffer: "Track | TrackGroup" = [] + current_tag_name, current_tag_count = tag_cup, 0 + + # every four 4 tracks, create a cup + for track in filter(lambda track: tag_cup in getattr(track, "tags", []), self._tracks): + track_buffer.append(track) + + if len(track_buffer) > 4: + current_tag_count += 1 + yield Cup(tracks=track_buffer, cup_id=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}") + + def get_unordered_cups(self) -> Generator["Cup", None, None]: + """ + Get all the cups with no cup tags + :return: cups with no cup tags + """ + # for track that have don't have a tag in self.tags_cups + track_buffer: "Track | TrackGroup" = [] + for track in filter( + lambda track: not any(item in getattr(track, "tags", []) for item in self.tags_cups), + self._tracks + ): + track_buffer.append(track) + + if len(track_buffer) > 4: + yield Cup(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([self.default_track] * (4 - len(track_buffer))) + yield Cup(tracks=track_buffer) + + def get_cups(self) -> Generator["Cup", None, None]: + """ + Get all the cups + :return: cups + """ + yield from self.get_ordered_cups() + yield from self.get_unordered_cups() + + def get_ctfile(self) -> str: + """ + Return the ct_file generated from the ModConfig + :return: ctfile content + """ + lecode_flags = filter(lambda v: v is not None, [ + "N$SHOW" if self.keep_original_track else "N$NONE", + "N$F_WII" if self.original_track_prefix else None, + "N$SWAP" if self.swap_original_order else None + ]) + + ctfile = ( + f"#CT-CODE\n" # magic number + f"[RACING-TRACK-LIST]\n" # start of the track section + f"%LE-FLAGS=1\n" # enable lecode mode + f"%WIIMM-CUP={int(self.enable_random_cup)}\n" # enable random cup + f"N {' | '.join(lecode_flags)}\n" # other flags to disable default tracks, ... + f"\n" + ) + + for cup in self.get_cups(): + ctfile += cup.get_ctfile(mod_config=self) + + return ctfile diff --git a/source/mkw/Track.py b/source/mkw/Track.py index c0b8cf7..ccec002 100644 --- a/source/mkw/Track.py +++ b/source/mkw/Track.py @@ -2,30 +2,11 @@ from typing import Generator import re from source.mkw import Tag, Slot +from source.safe_eval import safe_eval TOKEN_START = "{{" TOKEN_END = "}}" -common_token_map = { # these operators and function are considered safe to use in the template - operator: operator - for operator in - ["+", "-", "*", "/", "%", "**", ",", "(", ")", "[", "]", "==", "!=", "in", ">", "<", ">=", "<=", "and", "or", "&", - "|", "^", "~", "<<", ">>", "not", "is", "if", "else", "abs", "int", "bin", "hex", "oct", "chr", "ord", "len", - "str", "bool", "float", "round", "min", "max", "sum", "zip", "any", "all", "issubclass", "reversed", "enumerate", - "list", "sorted", "hasattr", "for", "range", "type", "isinstance", "repr", "None", "True", "False" - ] - -} | { # these methods are considered safe, except for the magic methods - f".{method}": f".{method}" - for method in dir(str) + dir(list) + dir(int) + dir(float) - if not method.startswith("__") -} - - -class TokenParsingError(Exception): - def __init__(self, token: str): - super().__init__(f"Invalid token while parsing track representation:\n{token}") - # representation of a custom track class Track: @@ -41,6 +22,9 @@ class Track: if key.startswith("__"): continue setattr(self, key, value) + def __repr__(self): + return f"" + @classmethod def from_dict(cls, track_dict: dict) -> "Track | TrackGroup": """ @@ -68,75 +52,24 @@ class Track: :return: formatted representation of the track """ - token_map = common_token_map | { # replace the suffix and the prefix by the corresponding values - "prefix": self.get_prefix(mod_config, ""), - "suffix": self.get_prefix(mod_config, ""), - } | { # replace the track attribute by the corresponding values - f"track.{attr}": f"track.{attr}" for attr, value in self.__dict__.items() - } | { # replace the track variable by the corresponding value, if not used with an attribute - "track": "track" - } + extra_token_map = { # replace the suffix and the prefix by the corresponding values + "prefix": f'{self.get_prefix(mod_config, "")!r}', + "suffix": f'{self.get_suffix(mod_config, "")!r}', + "track": "track" + } - def format_token(match: re.Match) -> str: - # get the token string without the brackets, then strip it - process_token = match.group(1).strip() - final_token: str = "" - - def matched(match: re.Match | str | None, value: str = None) -> bool: - """ - check if token is matched, if yes, add it to the final token and remove it from the processing token - :param match: match object - :param value: if the match is a string, the value to replace the text with - :return: True if matched, False otherwise - """ - nonlocal final_token, process_token - - # if there is no match or the string is empty, return False - if not match: return False - - if isinstance(match, re.Match): - process_token_raw = process_token[match.end():] - value = match.group() - - else: - if not process_token.startswith(match): return False - process_token_raw = process_token[len(match):] - - process_token = process_token_raw.lstrip() - - final_token += value + (len(process_token_raw) - len(process_token)) * " " - - return True - - while process_token: # while there is still tokens to process - # if the section is a string, add it to the final token - # example : "hello", "hello \" world" - if matched(re.match(r'^\"(?:[^"\\]|\\.)*\"', process_token)): - continue - - # if the section is a float or an int, add it to the final token - # example : 102, 102.59 - if matched(re.match(r'^[0-9]+(?:\.[0-9]+)?', process_token)): - continue - - # if the section is a variable, operator or function, replace it by its value - # example : track.special, + - for key, value in token_map.items(): - if matched(key, value): - break - - # else, the token is invalid, so raise an error - else: - raise TokenParsingError(process_token) - - # if final_token is set, eval final_token and return the result - if final_token: - return str(eval(final_token, {}, {"track": self})) - else: - return final_token + def format_template(match: re.Match) -> str: + """ + when a token is found, replace it by the corresponding value + :param match: match in the format + :return: corresponding value + """ + # get the token string without the brackets, then strip it. Also double antislash + template = match.group(1).strip().replace("\\", "\\\\") + return safe_eval(template, extra_token_map, {"track": self}) # pass everything between TOKEN_START and TOKEN_END in the function - return re.sub(rf"{TOKEN_START}(.*?){TOKEN_END}", format_token, format) + return re.sub(rf"{TOKEN_START}(.*?){TOKEN_END}", format_template, format) def get_prefix(self, mod_config: "ModConfig", default: any = None) -> any: """ @@ -145,8 +78,7 @@ class Track: :param mod_config: mod configuration :return: formatted representation of the track prefix """ - for tag in filter(lambda tag: tag in mod_config.tags_prefix, self.tags): - return mod_config.tags_prefix[tag] + for tag in filter(lambda tag: tag in mod_config.tags_prefix, self.tags): return tag return default def get_suffix(self, mod_config: "ModConfig", default: any = None) -> any: @@ -156,9 +88,29 @@ class Track: :param mod_config: mod configuration :return: formatted representation of the track suffix """ - for tag in filter(lambda tag: tag in mod_config.tags_suffix, self.tags): - return mod_config.tags_suffix[tag] + for tag in filter(lambda tag: tag in mod_config.tags_suffix, self.tags): return tag return default - def get_highlight(self, mod_config: "ModConfig", default: any = None) -> any: + def is_highlight(self, mod_config: "ModConfig", default: any = None) -> bool: ... + + def is_new(self, mod_config: "ModConfig", default: any = None) -> bool: + ... + + def get_ctfile(self, mod_config: "ModConfig", hidden: bool = False) -> str: + """ + return the ctfile of the track + :hidden: if the track is in a group + :return: ctfile + """ + # TODO: filename, info and - are not implemented + menu_name = f'{self.repr_format(mod_config=mod_config, format=mod_config.track_formatting["menu_name"])!r}' + file_name = f'{self.repr_format(mod_config=mod_config, format=mod_config.track_formatting["file_name"])!r}' + + 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, False) else 0):#04x}; ' # lecode flags + f'{file_name}; ' # filename + f'{menu_name}; ' # name of the track in the menu + f'{file_name}\n' # unique identifier for each track + ) diff --git a/source/mkw/TrackGroup.py b/source/mkw/TrackGroup.py index ce4cc86..e000d20 100644 --- a/source/mkw/TrackGroup.py +++ b/source/mkw/TrackGroup.py @@ -1,10 +1,13 @@ from typing import Generator +from source.mkw import Tag + -# representation of a group of tracks class TrackGroup: - def __init__(self, tracks: list["Track"] = None): + def __init__(self, tracks: list["Track"] = None, tags: list[Tag] = None, name: str = None): self.tracks = tracks if tracks is not None else [] + self.tags = tags if tags is not None else [] + self.name = name if name is not None else "" def get_tracks(self) -> Generator["Track", None, None]: """ @@ -24,4 +27,19 @@ class TrackGroup: from source.mkw.Track import Track if "group" not in group_dict: return Track.from_dict(group_dict) - return cls(tracks=[Track.from_dict(track) for track in group_dict["group"]]) + return cls( + tracks=[Track.from_dict(track) for track in group_dict["group"]], + tags=group_dict.get("tags"), + name=group_dict.get("name"), + ) + + def get_ctfile(self, mod_config: "ModConfig") -> 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(mod_config=mod_config, hidden=True) + + return ctfile diff --git a/source/safe_eval.py b/source/safe_eval.py new file mode 100644 index 0000000..1d9ff61 --- /dev/null +++ b/source/safe_eval.py @@ -0,0 +1,110 @@ +import re +from typing import Callable + +common_token_map = { # these operators and function are considered safe to use in the template + operator: operator + for operator in + ["+", "-", "*", "/", "%", "**", ",", "(", ")", "[", "]", "==", "!=", "in", ">", "<", ">=", "<=", "and", "or", "&", + "|", "^", "~", "<<", ">>", ":", "not", "is", "if", "else", "abs", "int", "bin", "hex", "oct", "chr", "ord", "len", + "str", "bool", "float", "round", "min", "max", "sum", "zip", "any", "all", "issubclass", "reversed", "enumerate", + "list", "sorted", "hasattr", "for", "range", "type", "isinstance", "repr", "None", "True", "False", "getattr" + ] +} | { # these methods are considered safe, except for the magic methods + f".{method}": f".{method}" + for method in dir(str) + dir(list) + dir(int) + dir(float) + if not method.startswith("__") +} + + +class TemplateParsingError(Exception): + def __init__(self, token: str): + super().__init__(f"Invalid token while parsing track representation:\n{token}") + + +class SafeFunction: + @classmethod + def get_all_safe_methods(cls) -> dict[str, Callable]: + """ + get all the safe methods defined by the class + :return: all the safe methods defined by the class + """ + return { + method: getattr(cls, method) + for method in dir(cls) + if not method.startswith("__") and method not in ["get_all_safe_methods"] + } + + @staticmethod + def getattr(obj: any, attr: str, default: any = None) -> any: + """ + Safe getattr, raise an error if the attribute is a function + :param obj: object to get the attribute from + :param attr: attribute name + :param default: default value if the attribute is not found + :return: the attribute value + """ + attr = getattr(obj, attr) if default is None else getattr(obj, attr, default) + if callable(attr): raise AttributeError(f"getattr can't be used for functions (tried: tr{attr})") + return attr + + +def safe_eval(template: str, extra_token_map: dict[str, str] = None, env: dict[str, any] = None) -> str: + """ + Evaluate the template and return the result in a safe way + :param extra_token_map: additionnal tokens to use in the template + :param env: variables to use when using eval + :param template: template to evaluate + """ + if extra_token_map is None: extra_token_map = {} + if env is None: env = {} + + token_map: dict[str, str] = common_token_map | extra_token_map + final_token: str = "" + + def matched(match: re.Match | str | None, value: str = None) -> bool: + """ + check if token is matched, if yes, add it to the final token and remove it from the processing token + :param match: match object + :param value: if the match is a string, the value to replace the text with + :return: True if matched, False otherwise + """ + nonlocal final_token, template + + # if there is no match or the string is empty, return False + if not match: return False + + if isinstance(match, re.Match): + template_raw = template[match.end():] + value = match.group() + + else: + if not template.startswith(match): return False + template_raw = template[len(match):] + + template = template_raw.lstrip() + final_token += value + (len(template_raw) - len(template)) * " " + return True + + while template: # while there is still tokens to process + # if the section is a string, add it to the final token + # example : "hello", "hello \" world" + if matched(re.match(r'^(["\'])((\\{2})*|(.*?[^\\](\\{2})*))\1', template)): + continue + + # if the section is a float or an int, add it to the final token + # example : 102, 102.59 + if matched(re.match(r'^[0-9]+(?:\.[0-9]+)?', template)): + continue + + # if the section is a variable, operator or function, replace it by its value + # example : track.special, + + for key, value in token_map.items(): + if matched(key, value): break + + # else, the token is invalid, so raise an error + else: + raise TemplateParsingError(template) + + # if final_token is set, eval final_token and return the result + if final_token: return str(eval(final_token, SafeFunction.get_all_safe_methods(), env)) + else: return final_token diff --git a/source/wt/wit.py b/source/wt/wit.py index 764b932..ea8ed46 100644 --- a/source/wt/wit.py +++ b/source/wt/wit.py @@ -13,9 +13,9 @@ class Extension(enum.Enum): """ Enum for game extension """ - FST = ".dol" WBFS = ".wbfs" ISO = ".iso" + FST = ".dol" @classmethod def _missing_(cls, value: str) -> "Extension | None":