mirror of
https://github.com/Faraphel/Atlas-Install.git
synced 2025-07-03 03:08:29 +02:00
implemented ModConfig (new version of CtConfig), Track and TrackGroup
This commit is contained in:
parent
a04f7286b6
commit
a83ce2c9c1
6 changed files with 396 additions and 6 deletions
|
@ -5,7 +5,7 @@
|
|||
"game_variant":"60",
|
||||
"region":5500,
|
||||
"cheat_region":20061,
|
||||
"prefix_list":[
|
||||
"tags_prefix":[
|
||||
"Wii U",
|
||||
"3DS",
|
||||
"DS",
|
||||
|
@ -57,7 +57,7 @@
|
|||
"GK7",
|
||||
"FGKR"
|
||||
],
|
||||
"suffix_list":[
|
||||
"tags_suffix":[
|
||||
"Boost"
|
||||
],
|
||||
"tags_color":{
|
||||
|
@ -114,7 +114,9 @@
|
|||
"GK3":"green",
|
||||
"GK7":"green"
|
||||
},
|
||||
|
||||
"tag_retro":"Retro",
|
||||
|
||||
"default_track":{
|
||||
"music":"T32",
|
||||
"special":"T32",
|
||||
|
@ -1953,8 +1955,8 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"!default_sort":"name",
|
||||
"tracks_list":[
|
||||
|
||||
"tracks":[
|
||||
{
|
||||
"name":"4IT Clown's Road",
|
||||
"author":[
|
||||
|
|
|
@ -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()
|
||||
|
|
@ -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:
|
||||
...
|
|
@ -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"]])
|
|
@ -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
|
||||
}
|
||||
|
|
@ -4,10 +4,14 @@ tools_path = tools_szs_dir / "wszst.exe"
|
|||
|
||||
|
||||
class SZSPath:
|
||||
__slots__ = ("path",)
|
||||
|
||||
def __init__(self, path: Path | str):
|
||||
self.path: Path = path if isinstance(path, Path) else Path(path)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SZSPath: {self.path}>"
|
||||
|
||||
@better_error(tools_path)
|
||||
def _run(self, *args) -> bytes:
|
||||
"""
|
||||
|
@ -37,8 +41,94 @@ class SZSPath:
|
|||
:param dest: destination 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:
|
||||
file.write(self.cat(subfile))
|
||||
file.write(self.szs_path.cat(self.subfile))
|
||||
|
||||
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]
|
||||
|
|
Loading…
Reference in a new issue