started restructuring pack (part 3), added ct_icons generation, added ExtractedGame class alongside Game, started install_all_patch function

This commit is contained in:
Faraphel 2022-06-14 14:27:48 +02:00
parent 3402a9b26c
commit d5ef16611d
27 changed files with 207 additions and 53 deletions

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -44,7 +44,7 @@
"SK64": "green", "SK64": "green",
"SMG": "red2", "SMG": "red2",
"Spyro 1": "blue", "Spyro 1": "blue",
"Wii U": "red4", "Switch": "red4",
"Wii": "blue", "Wii": "blue",
"3DS": "YOR3", "3DS": "YOR3",
"DS": "white", "DS": "white",
@ -65,7 +65,7 @@
"tags_suffix": { "tags_suffix": {
"Boost": "orange" "Boost": "orange"
}, },
"tags_cups": ["Wii U", "3DS", "DS", "GCN", "GBA", "N64", "SNES", "MKT", "RMX", "DX", "GP"], "tags_cups": ["Switch", "3DS", "DS", "GCN", "GBA", "N64", "SNES", "MKT", "RMX", "DX", "GP"],
"track_formatting": { "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 '' }}", "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 '' }}",
@ -92,7 +92,7 @@
"score":3, "score":3,
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -106,7 +106,7 @@
"version":"RC1", "version":"RC1",
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -120,7 +120,7 @@
"version":"v1.2b", "version":"v1.2b",
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -134,7 +134,7 @@
"version":"v1", "version":"v1",
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -148,7 +148,7 @@
"version":"RC1", "version":"RC1",
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -162,7 +162,7 @@
"score":4, "score":4,
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -176,7 +176,7 @@
"score":3, "score":3,
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
], ],
"note":"some collision (like the plane) are buggy, and the texture feel a bit too dark" "note":"some collision (like the plane) are buggy, and the texture feel a bit too dark"
}, },
@ -191,7 +191,7 @@
"version":"v1.1", "version":"v1.1",
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -205,7 +205,7 @@
"score":5, "score":5,
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -219,7 +219,7 @@
"score":5, "score":5,
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -233,7 +233,7 @@
"score":5, "score":5,
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
] ]
}, },
{ {
@ -245,7 +245,7 @@
"since_version":"0.1", "since_version":"0.1",
"tags":[ "tags":[
"Retro", "Retro",
"Wii U" "Switch"
], ],
"sha1":"e782de12471731e9bb155eea6872cd5b2303357d", "sha1":"e782de12471731e9bb155eea6872cd5b2303357d",
"version":"v1.3" "version":"v1.3"

View file

@ -1,32 +1,64 @@
# class that represent a mario kart wii cup # class that represent a mario kart wii cup
from PIL import Image from PIL import Image, ImageDraw, ImageFont
class Cup: class Cup:
__slots__ = ["_tracks", "cup_id"] __slots__ = ["_tracks", "cup_name", "cup_id"]
_last_cup_id = 0 _last_cup_id = 0
def __init__(self, tracks: list["Track | TrackGroup"], cup_id: str | None = None): def __init__(self, tracks: list["Track | TrackGroup"], cup_name: str | None = None):
self._tracks = tracks[:4] self._tracks = tracks[:4]
if cup_id is None: self.cup_id = self.__class__._last_cup_id
cup_id = self.__class__._last_cup_id self.cup_name = cup_name if cup_name is not None else self.cup_id
self.__class__._last_cup_id += 1 self.__class__._last_cup_id += 1
self.cup_id = cup_id
def __repr__(self): def __repr__(self):
return f"<Cup id={self.cup_id} tracks={self._tracks}>" return f"<Cup name={self.cup_name} id={self.cup_id} tracks={self._tracks}>"
def get_cup_icon(self) -> Image.Image: def get_default_cticon(self, mod_config: "ModConfig") -> Image.Image:
... """
Get the default cticon for this cup
:return: the default cticon
"""
ct_icon = Image.new("RGBA", (128, 128))
draw = ImageDraw.Draw(ct_icon)
draw.text(
(4, 4),
"CT",
(255, 165, 0),
font=ImageFont.truetype(mod_config.get_default_font(), 90),
stroke_width=2,
stroke_fill=(0, 0, 0)
)
draw.text(
(5, 80),
f"{self.cup_id:03}",
(255, 165, 0),
font=ImageFont.truetype(mod_config.get_default_font(), 60),
stroke_width=2,
stroke_fill=(0, 0, 0)
)
return ct_icon
def get_cticon(self, mod_config: "ModConfig") -> Image.Image:
"""
Get the cticon for this cup
:return: the cticon
"""
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)
def get_ctfile(self, mod_config: "ModConfig") -> str: def get_ctfile(self, mod_config: "ModConfig") -> str:
""" """
Get the ctfile for this cup Get the ctfile for this cup
:return: the ctfile :return: the ctfile
""" """
ctfile = f'C "{self.cup_id}"\n' ctfile = f'C "{self.cup_name}"\n'
for track in self._tracks: ctfile += track.get_ctfile(mod_config=mod_config) for track in self._tracks: ctfile += track.get_ctfile(mod_config=mod_config)
ctfile += "\n" ctfile += "\n"

View file

@ -1,3 +1,4 @@
import json
from pathlib import Path from pathlib import Path
from typing import Generator from typing import Generator
@ -6,25 +7,69 @@ from source.wt import szs
from source.wt.wit import WITPath, Region, Extension from source.wt.wit import WITPath, Region, Extension
def extract_autoadd(extracted_game: Path | str, destination_path: Path | str) -> Path: class ExtractedGame:
""" """
Extract all the autoadd files from the game to destination_path Class that represents an extracted game
:param extracted_game: path of the extracted game
:param destination_path: directory where the autoadd files will be extracted
:return: directory where the autoadd files were extracted
""" """
yield {"description": "Extracting autoadd files...", "determinate": False} def __init__(self, path: Path | str):
szs.autoadd(extracted_game / "files/Race/Course/", destination_path) self.path = Path(path)
def extract_autoadd(self, destination_path: Path | str) -> Generator[dict, None, None]:
"""
Extract all the autoadd files from the game to destination_path
:param destination_path: directory where the autoadd files will be extracted
:return: directory where the autoadd files were extracted
"""
yield {"description": "Extracting autoadd files...", "determinate": False}
szs.autoadd(self.path / "files/Race/Course/", destination_path)
def install_mystuff(extracted_game: Path | str) -> None: def install_mystuff(self) -> Generator[dict, None, None]:
""" """
Install mystuff directory Install mystuff directory
:param extracted_game: the extracted game :return:
:return: """
""" yield {"description": "Installing MyStuff directory...", "determinate": False}
yield {"description": "Installing MyStuff directory...", "determinate": False} ...
def install_file(self, mod_config: ModConfig, patch_directory: Path | str, subfile: Path | str) \
-> Generator[dict, None, None]:
"""
Install a file into the game
:param patch_directory: patch_directory where the subfile is located
:param subfile: subfile to install
:param mod_config: the mod to install
"""
subfile = Path(subfile)
yield {"description": f"Installing {subfile.name}...", "determinate": False}
configuration = {}
configuration_path = subfile.with_suffix(subfile.suffix + ".json")
if configuration_path.exists(): configuration |= json.loads(configuration_path.read_text(encoding="utf8"))
def install_patch(self, mod_config: ModConfig, patch_directory: Path | str) -> Generator[dict, None, None]:
"""
Install a patch into the game
:param mod_config: the mod to install
:param patch_directory: directory containing the patch
"""
patch_directory = Path(patch_directory)
yield {"description": f"Installing Patch {patch_directory.parent.name}...", "determinate": False}
for subfile in filter(lambda sf: sf.suffix == ".json", patch_directory.rglob("*")):
self.install_file(mod_config, subfile)
def install_all_patch(self, mod_config: ModConfig) -> Generator[dict, None, None]:
"""
Install all patchs of the mod_config into the game
:param mod_config: the mod to install
:return:
"""
yield {"description": "Installing all Patch...", "determinate": False}
# for all directory that are in the root of the mod, and don't start with an underscore,
# for all of the subdirectory named "_PATCH", apply the patch
for patch_directory in mod_config.get_mod_directory().glob("[!_]*").rglob("_PATCH/"):
self.install_patch(mod_config, patch_directory)
class Game: class Game:
@ -63,9 +108,11 @@ class Game:
except StopIteration as e: except StopIteration as e:
return e.value return e.value
def get_output_directory(self, dest: Path | str, mod_config: ModConfig) -> Path: @staticmethod
def get_output_directory(dest: Path | str, mod_config: ModConfig) -> Path:
""" """
Return the directory where the game will be installed Return the directory where the game will be installed
:param dest: destination directory
:param mod_config: mod configuration :param mod_config: mod configuration
:return: directory where the game will be installed :return: directory where the game will be installed
""" """
@ -94,9 +141,10 @@ class Game:
cache_directory.mkdir(parents=True, exist_ok=True) cache_directory.mkdir(parents=True, exist_ok=True)
# get the directory where the game will be extracted # get the directory where the game will be extracted
extracted_game: Path = self.get_output_directory(dest, mod_config) extracted_game = ExtractedGame(self.get_output_directory(dest, mod_config))
yield from self.extract(extracted_game) yield from self.extract(extracted_game.path)
yield from extract_autoadd(extracted_game, cache_directory / "autoadd/") yield from extracted_game.extract_autoadd(cache_directory / "autoadd/")
yield from install_mystuff(extracted_game) yield from extracted_game.install_mystuff()
yield from extracted_game.install_all_patch(mod_config)

View file

@ -1,6 +1,8 @@
from pathlib import Path from pathlib import Path
from typing import Generator from typing import Generator
from PIL import Image
from source.mkw import Tag, Color from source.mkw import Tag, Color
from source.mkw.Cup import Cup from source.mkw.Cup import Cup
from source.mkw.Track import Track from source.mkw.Track import Track
@ -9,11 +11,11 @@ import json
# representation of the configuration of a mod # representation of the configuration of a mod
class ModConfig: class ModConfig:
__slots__ = ("name", "nickname", "variant", "region", "tags_prefix", "tags_suffix", __slots__ = ("name", "path", "nickname", "variant", "region", "tags_prefix", "tags_suffix",
"default_track", "_tracks", "version", "original_track_prefix", "swap_original_order", "default_track", "_tracks", "version", "original_track_prefix", "swap_original_order",
"keep_original_track", "enable_random_cup", "tags_cups", "track_formatting") "keep_original_track", "enable_random_cup", "tags_cups", "track_formatting")
def __init__(self, name: str, nickname: str = None, version: str = None, variant: str = None, def __init__(self, path: Path | str, name: str, nickname: str = None, version: str = None, variant: str = None,
tags_prefix: dict[Tag, Color] = None, tags_suffix: dict[Tag, Color] = None, tags_prefix: dict[Tag, Color] = None, tags_suffix: dict[Tag, Color] = None,
tags_cups: list[Tag] = None, region: dict[int] | int = None, tags_cups: list[Tag] = None, region: dict[int] | int = None,
default_track: "Track | TrackGroup" = None, tracks: list["Track | TrackGroup"] = None, default_track: "Track | TrackGroup" = None, tracks: list["Track | TrackGroup"] = None,
@ -21,6 +23,8 @@ class ModConfig:
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): track_formatting: dict[str, str] = None):
self.path = Path(path)
self.name: str = name self.name: str = name
self.nickname: str = nickname if nickname is not None else name self.nickname: str = nickname if nickname is not None else name
self.version: str = version if version is not None else "v1.0.0" self.version: str = version if version is not None else "v1.0.0"
@ -48,20 +52,22 @@ class ModConfig:
return f"<ModConfig name={self.name} version={self.version}>" return f"<ModConfig name={self.name} version={self.version}>"
@classmethod @classmethod
def from_dict(cls, config_dict: dict) -> "ModConfig": def from_dict(cls, path: Path | str, config_dict: dict) -> "ModConfig":
""" """
Create a ModConfig from a dict Create a ModConfig from a dict
:param path: path of the mod_config.json
:param config_dict: dict containing the configuration :param config_dict: dict containing the configuration
:return: ModConfig :return: ModConfig
""" """
kwargs = { kwargs = {
attr: config_dict.get(attr) attr: config_dict.get(attr)
for attr in cls.__slots__ for attr in cls.__slots__
if attr not in ["name", "default_track", "_tracks", "tracks"] if attr not in ["name", "default_track", "_tracks", "tracks", "path"]
# these keys are treated after or are reserved # these keys are treated after or are reserved
} }
return cls( return cls(
path=Path(path),
name=config_dict["name"], name=config_dict["name"],
**kwargs, **kwargs,
@ -77,8 +83,18 @@ class ModConfig:
:param config_file: file containing the configuration :param config_file: file containing the configuration
:return: ModConfig :return: ModConfig
""" """
if isinstance(config_file, str): config_file = Path(config_file) config_file = Path(config_file)
return cls.from_dict(json.loads(config_file.read_text(encoding="utf8"))) return cls.from_dict(
path=config_file,
config_dict=json.loads(config_file.read_text(encoding="utf8"))
)
def get_mod_directory(self) -> Path:
"""
Get the directory of the mod
:return: directory of the mod
"""
return self.path.parent
def get_tracks(self) -> Generator["Track", None, None]: def get_tracks(self) -> Generator["Track", None, None]:
""" """
@ -105,13 +121,13 @@ class ModConfig:
if len(track_buffer) > 4: if len(track_buffer) > 4:
current_tag_count += 1 current_tag_count += 1
yield Cup(tracks=track_buffer, cup_id=f"{current_tag_name}-{current_tag_count}") yield Cup(tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count}")
track_buffer = [] track_buffer = []
# if there is still tracks in the buffer, create a cup with them and fill with default> # if there is still tracks in the buffer, create a cup with them and fill with default>
if len(track_buffer) > 0: if len(track_buffer) > 0:
track_buffer.extend([self.default_track] * (4 - len(track_buffer))) 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}") yield Cup(tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count+1}")
def get_unordered_cups(self) -> Generator["Cup", None, None]: def get_unordered_cups(self) -> Generator["Cup", None, None]:
""" """
@ -167,3 +183,61 @@ class ModConfig:
ctfile += cup.get_ctfile(mod_config=self) ctfile += cup.get_ctfile(mod_config=self)
return ctfile return ctfile
def get_base_cticons(self) -> Generator[Image.Image, None, None]:
"""
Return the base cticon
:return:
"""
icon_names = ["left", "right"]
if self.keep_original_track:
icon_names += [
f"_DEFAULT/{name}"
for name in (
["mushroom", "shell", "flower", "banana", "star", "leaf", "special", "lightning"]
if self.swap_original_order else
["mushroom", "flower", "star", "special", "shell", "banana", "leaf", "lightning"]
)
]
if self.enable_random_cup: icon_names.append("random")
for icon_name in icon_names:
yield Image.open(self.get_mod_directory() / f"{icon_name}.png").resize((128, 128))
def get_custom_cticons(self) -> Generator[Image.Image, None, None]:
"""
Return the custom ct_icon generated from the ModConfig
:return:
"""
for cup in self.get_cups():
yield cup.get_cticon(mod_config=self).resize((128, 128))
def get_cticons(self) -> Generator[Image.Image, None, None]:
"""
Return all the ct_icon generated from the ModConfig
:return:
"""
yield from self.get_base_cticons()
yield from self.get_custom_cticons()
@staticmethod
def get_default_font() -> Path:
"""
Return the default font for creating ct_icons
:return: the path to the default font file
"""
# TODO: make it customizable
return Path("./assets/SuperMario256.ttf")
def get_full_cticon(self) -> Image.Image:
"""
Return the full ct_icon generated from the ModConfig
:return:
"""
cticons = list(self.get_cticons())
full_cticon = Image.new("RGBA", (128 * len(cticons), 128))
for i, cticon in enumerate(cticons): full_cticon.paste(cticon, (i * 128, 0))
return full_cticon