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",
|
"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":[
|
||||||
|
|
|
@ -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:
|
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]
|
||||||
|
|
Loading…
Reference in a new issue