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

View file

@ -1,32 +1,64 @@
# class that represent a mario kart wii cup
from PIL import Image
from PIL import Image, ImageDraw, ImageFont
class Cup:
__slots__ = ["_tracks", "cup_id"]
__slots__ = ["_tracks", "cup_name", "cup_id"]
_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]
if cup_id is None:
cup_id = self.__class__._last_cup_id
self.__class__._last_cup_id += 1
self.cup_id = cup_id
self.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
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:
"""
Get the ctfile for this cup
: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)
ctfile += "\n"

View file

@ -1,3 +1,4 @@
import json
from pathlib import Path
from typing import Generator
@ -6,25 +7,69 @@ from source.wt import szs
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
: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
Class that represents an extracted game
"""
yield {"description": "Extracting autoadd files...", "determinate": False}
szs.autoadd(extracted_game / "files/Race/Course/", destination_path)
def __init__(self, path: Path | str):
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:
"""
Install mystuff directory
:param extracted_game: the extracted game
:return:
"""
yield {"description": "Installing MyStuff directory...", "determinate": False}
def install_mystuff(self) -> Generator[dict, None, None]:
"""
Install mystuff directory
:return:
"""
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:
@ -63,9 +108,11 @@ class Game:
except StopIteration as e:
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
:param dest: destination directory
:param mod_config: mod configuration
:return: directory where the game will be installed
"""
@ -94,9 +141,10 @@ class Game:
cache_directory.mkdir(parents=True, exist_ok=True)
# 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 extract_autoadd(extracted_game, cache_directory / "autoadd/")
yield from install_mystuff(extracted_game)
yield from self.extract(extracted_game.path)
yield from extracted_game.extract_autoadd(cache_directory / "autoadd/")
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 typing import Generator
from PIL import Image
from source.mkw import Tag, Color
from source.mkw.Cup import Cup
from source.mkw.Track import Track
@ -9,11 +11,11 @@ import json
# representation of the configuration of a mod
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",
"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_cups: list[Tag] = None, region: dict[int] | int = 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,
track_formatting: dict[str, str] = None):
self.path = Path(path)
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 "v1.0.0"
@ -48,20 +52,22 @@ class ModConfig:
return f"<ModConfig name={self.name} version={self.version}>"
@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
:param path: path of the mod_config.json
:param config_dict: dict containing the configuration
:return: ModConfig
"""
kwargs = {
attr: config_dict.get(attr)
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
}
return cls(
path=Path(path),
name=config_dict["name"],
**kwargs,
@ -77,8 +83,18 @@ class ModConfig:
: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")))
config_file = Path(config_file)
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]:
"""
@ -105,13 +121,13 @@ class ModConfig:
if len(track_buffer) > 4:
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 = []
# 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}")
yield Cup(tracks=track_buffer, cup_name=f"{current_tag_name}/{current_tag_count+1}")
def get_unordered_cups(self) -> Generator["Cup", None, None]:
"""
@ -167,3 +183,61 @@ class ModConfig:
ctfile += cup.get_ctfile(mod_config=self)
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