started restructuring pack (part 3), added ct_icons generation, added ExtractedGame class alongside Game, started install_all_patch function
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|