implemented ModConfig (new version of CtConfig), Track and TrackGroup

This commit is contained in:
Faraphel 2022-06-09 16:49:46 +02:00
parent a04f7286b6
commit a83ce2c9c1
6 changed files with 396 additions and 6 deletions

View file

@ -5,7 +5,7 @@
"game_variant":"60", "game_variant":"60",
"region":5500, "region":5500,
"cheat_region":20061, "cheat_region":20061,
"prefix_list":[ "tags_prefix":[
"Wii U", "Wii U",
"3DS", "3DS",
"DS", "DS",
@ -57,7 +57,7 @@
"GK7", "GK7",
"FGKR" "FGKR"
], ],
"suffix_list":[ "tags_suffix":[
"Boost" "Boost"
], ],
"tags_color":{ "tags_color":{
@ -114,7 +114,9 @@
"GK3":"green", "GK3":"green",
"GK7":"green" "GK7":"green"
}, },
"tag_retro":"Retro", "tag_retro":"Retro",
"default_track":{ "default_track":{
"music":"T32", "music":"T32",
"special":"T32", "special":"T32",
@ -1953,8 +1955,8 @@
] ]
} }
], ],
"!default_sort":"name",
"tracks_list":[ "tracks":[
{ {
"name":"4IT Clown's Road", "name":"4IT Clown's Road",
"author":[ "author":[

View file

@ -0,0 +1,81 @@
from pathlib import Path
from typing import Generator
from source.mkw import Tag, Color
from source.mkw.Track import Track
import json
# representation of the configuration of a mod
class ModConfig:
__slots__ = ("name", "nickname", "variant", "region", "tags_prefix", "tags_suffix", "tag_retro",
"default_track", "_tracks", "version", "original_track_prefix", "swap_original_order",
"keep_original_track", "enable_random_cup")
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,
region: dict[int] | int = None, tag_retro: 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):
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.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[str] = tags_prefix if tags_prefix is not None else {}
self.tags_suffix: dict[str] = tags_suffix if tags_suffix is not None else {}
self.tag_retro: str = tag_retro if tag_retro is None else "Retro"
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.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
@classmethod
def from_dict(cls, config_dict: dict) -> "ModConfig":
"""
Create a ModConfig from a dict
:param config_dict: dict containing the configuration
:return: ModConfig
"""
return cls(
name=config_dict["name"],
nickname=config_dict.get("nickname"),
version=config_dict.get("version"),
variant=config_dict.get("variant"),
tags_prefix=config_dict.get("tags_prefix"),
tags_suffix=config_dict.get("tags_suffix"),
tag_retro=config_dict.get("tag_retro"),
original_track_prefix=config_dict.get("original_track_prefix"),
swap_original_order=config_dict.get("swap_original_order"),
keep_original_track=config_dict.get("keep_original_track"),
enable_random_cup=config_dict.get("enable_random_cup"),
default_track=Track.from_dict(config_dict.get("default_track", {})),
tracks=[Track.from_dict(track) for track in config_dict.get("tracks", [])],
)
@classmethod
def from_file(cls, config_file: str | Path) -> "ModConfig":
"""
Create a ModConfig from a file
: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")))
def get_tracks(self) -> Generator["Track", None, None]:
"""
Get all the track elements
:return: track elements
"""
for track in self._tracks:
yield from track.get_tracks()

View file

@ -0,0 +1,162 @@
from typing import Generator
import re
from source.mkw import Tag, Slot
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:
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():
setattr(self, key, value)
@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", format: str) -> str:
"""
return the representation of the track from the format
:param mod_config: configuration of the mod
: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"
}
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
# pass everything between TOKEN_START and TOKEN_END in the function
return re.sub(rf"{TOKEN_START}(.*?){TOKEN_END}", format_token, format)
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
"""
for tag in filter(lambda tag: tag in mod_config.tags_prefix, self.tags):
return mod_config.tags_prefix[tag]
return 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
"""
for tag in filter(lambda tag: tag in mod_config.tags_suffix, self.tags):
return mod_config.tags_suffix[tag]
return default
def get_highlight(self, mod_config: "ModConfig", default: any = None) -> any:
...

View file

@ -0,0 +1,27 @@
from typing import Generator
# representation of a group of tracks
class TrackGroup:
def __init__(self, tracks: list["Track"] = None):
self.tracks = tracks if tracks is not None else []
def get_tracks(self) -> Generator["Track", None, None]:
"""
Get all the track elements
:return: track elements
"""
for track in self.tracks:
yield from track.get_tracks()
@classmethod
def from_dict(cls, group_dict: dict) -> "TrackGroup | Track":
"""
create a track group from a dict, or create a track from the dict if not a group
:param group_dict: dict containing the track information
:return: TrackGroup or Track
"""
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"]])

View file

@ -0,0 +1,28 @@
import enum
Tag = str
Slot = str
Color = {
"yor7": 0xF5090B, # apple red
"yor6": 0xE82C09, # dark red
"yor5": 0xE65118, # dark orange (flame)
"yor4": 0xFF760E, # orange (pumpkin)
"yor3": 0xFFA61F, # light orange (bright yellow)
"yor2": 0xFEBC1F, # yellow (ripe mango)
"yor1": 0xFFE71F, # light yellow
"yor0": 0xFFFF22, # neon yellow
"blue2": 0x1170EC, # dark blue
"blue1": 0x75B5F6, # azure
"green": 0x0EB00A, # green
"yellow": 0xFFFD1E, # neon yellow 2
"red4": 0xEE0C10, # vivid red
"red3": 0xFF0308, # red
"red2": 0xF14A4E, # light red
"red1": 0xE46C74, # pink
"white": 0xFFFFFF, # white
"clear": 0x000000, # clear
"off": 0x998C86 # off
}

View file

@ -4,10 +4,14 @@ tools_path = tools_szs_dir / "wszst.exe"
class SZSPath: class SZSPath:
__slots__ = ("path",)
def __init__(self, path: Path | str): def __init__(self, path: Path | str):
self.path: Path = path if isinstance(path, Path) else Path(path) self.path: Path = path if isinstance(path, Path) else Path(path)
def __repr__(self):
return f"<SZSPath: {self.path}>"
@better_error(tools_path) @better_error(tools_path)
def _run(self, *args) -> bytes: def _run(self, *args) -> bytes:
""" """
@ -37,8 +41,94 @@ class SZSPath:
:param dest: destination path :param dest: destination path
:return: the extracted file path :return: the extracted file path
""" """
dest = dest if isinstance(dest, Path) else Path(dest) return self[subfile].extract(dest)
def extract_all(self, dest: Path | str) -> Path:
"""
Extract all the subfiles to a destination
:param dest: output directory
:return:
"""
dest: Path = dest if isinstance(dest, Path) else Path(dest)
if dest.is_dir(): dest /= self.path.name
self._run("EXTRACT", self.path, "-d", dest)
return dest
def analyze(self) -> dict:
"""
Return the analyze of the szs
:return: dictionnary of key and value of the analyze
"""
analyze = {}
for line in filter(lambda f: f.strip(), self._run("ANALYZE", self.path).decode().splitlines()):
key, value = line.split("=", 1)
analyze[key.strip()] = value.strip()
return analyze
def list_raw(self) -> list[str]:
"""
Return the list of subfiles
:return: the list of subfiles
"""
# cycle though all of the output line of the command, check if the line are empty, and if not,
# add it to the list. Finally, remove the first line because this is a description of the command
return [subfile.strip() for subfile in self._run("list", self.path).decode().splitlines() if subfile][1:]
def list(self) -> list["SZSSubPath"]:
"""
Return the list of subfiles
:return: the list of subfiles
"""
return [self.get_subfile(subfile) for subfile in self.list_raw()]
def get_subfile(self, subfile: str) -> "SZSSubPath":
"""
Return the subfile of the szs
:return: the subfile
"""
return SZSSubPath(self, subfile)
def __getitem__(self, item):
return self.get_subfile(item)
def __iter__(self):
return iter(self.list())
class SZSSubPath:
__slots__ = ("szs_path", "subfile")
def __init__(self, szs_path: SZSPath, subfile: str):
self.szs_path = szs_path
self.subfile = subfile
def __repr__(self):
return f"<SZSSubPath: {self.szs_path.path}/{self.subfile}>"
def extract(self, dest: Path | str) -> Path:
"""
Extract the subfile to a destination
:param dest: destination path
:return: the extracted file path
"""
if self.is_dir(): raise ValueError("Can't extract a directory")
dest: Path = dest if isinstance(dest, Path) else Path(dest)
if dest.is_dir(): dest /= self.basename()
with dest.open("wb") as file: with dest.open("wb") as file:
file.write(self.cat(subfile)) file.write(self.szs_path.cat(self.subfile))
return dest return dest
def is_dir(self):
return self.subfile.endswith("/")
def is_file(self):
return not self.is_dir()
def basename(self):
return self.subfile.rsplit("/", 1)[-1]