mirror of
https://github.com/Faraphel/Atlas-Install.git
synced 2025-07-03 19:28:25 +02:00
Added docstring and more type hint tp Game, Gui and Track to make the code more understandable
This commit is contained in:
parent
ee8fea9c5f
commit
a6ec86c61d
3 changed files with 197 additions and 72 deletions
147
source/Game.py
147
source/Game.py
|
@ -1,4 +1,4 @@
|
|||
from tkinter import messagebox
|
||||
from tkinter import messagebox, StringVar, BooleanVar, IntVar
|
||||
from PIL import Image
|
||||
import shutil
|
||||
import glob
|
||||
|
@ -40,8 +40,32 @@ class CantConvertTrack(Exception):
|
|||
super().__init__("Can't convert track, check if download are enabled.")
|
||||
|
||||
|
||||
class NoGui:
|
||||
"""
|
||||
'fake' gui if no gui are used for compatibility.
|
||||
"""
|
||||
def progression(self, *args, **kwargs): print(args, kwargs)
|
||||
def translate(self, *args, **kwargs): return ""
|
||||
def log_error(self, *args, **kwargs): print(args, kwargs)
|
||||
|
||||
is_dev_version = False
|
||||
|
||||
stringvar_game_format = StringVar()
|
||||
boolvar_disable_download = BooleanVar()
|
||||
intvar_process_track = IntVar()
|
||||
boolvar_dont_check_track_sha1 = BooleanVar()
|
||||
boolvar_del_track_after_conv = BooleanVar()
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self, path: str = "", region_ID: str = "P", game_ID: str = "RMCP01", gui=None):
|
||||
"""
|
||||
Class about the game code and its treatment.
|
||||
:param path: path of the game file / directory
|
||||
:param region_ID: game's region id (P for PAL, K for KOR, ...)
|
||||
:param game_ID: game's id (RMCP01 for PAL, ...)
|
||||
:param gui: gui class used by the program
|
||||
"""
|
||||
if not os.path.exists(path) and path: raise InvalidGamePath()
|
||||
self.extension = None
|
||||
self.path = path
|
||||
|
@ -49,17 +73,21 @@ class Game:
|
|||
self.region = region_id_to_name[region_ID]
|
||||
self.region_ID = region_ID
|
||||
self.game_ID = game_ID
|
||||
self.gui = gui
|
||||
self.gui = gui if gui else NoGui
|
||||
self.ctconfig = CT_Config(gui=gui)
|
||||
|
||||
def set_path(self, path):
|
||||
def set_path(self, path: str) -> None:
|
||||
"""
|
||||
Change game path
|
||||
:param path: game's file
|
||||
"""
|
||||
self.extension = get_extension(path).upper()
|
||||
self.path = path
|
||||
|
||||
def convert_to(self, format: str = "FST"):
|
||||
def convert_to(self, format: str = "FST") -> None:
|
||||
"""
|
||||
Convert game to an another format
|
||||
:param format: game format (ISO, WBFS, ...)
|
||||
:return: converted game path
|
||||
"""
|
||||
if format in ["ISO", "WBFS", "CISO"]:
|
||||
path_game_format: str = os.path.realpath(self.path + "/../MKWFaraphel." + format.lower())
|
||||
|
@ -70,7 +98,10 @@ class Game:
|
|||
self.gui.progress(statut=self.gui.translate("Changing game's ID"), add=1)
|
||||
wszst.edit(self.path, region_ID=self.region_ID, name=f"Mario Kart Wii Faraphel {self.ctconfig.version}")
|
||||
|
||||
def extract(self):
|
||||
def extract(self) -> None:
|
||||
"""
|
||||
Extract game file in the same directory.
|
||||
"""
|
||||
if self.extension == "DOL":
|
||||
self.path = os.path.realpath(self.path + "/../../") # main.dol is in PATH/sys/, so go back 2 dir upper
|
||||
|
||||
|
@ -104,6 +135,9 @@ class Game:
|
|||
|
||||
@in_thread
|
||||
def install_mod(self):
|
||||
"""
|
||||
Patch the game to install the mod
|
||||
"""
|
||||
try:
|
||||
with open("./fs.json") as f:
|
||||
fs = json.load(f)
|
||||
|
@ -204,14 +238,22 @@ class Game:
|
|||
finally:
|
||||
self.gui.progress(show=False)
|
||||
|
||||
def patch_autoadd(self, auto_add_dir: str = "./file/auto-add"):
|
||||
def patch_autoadd(self, auto_add_dir: str = "./file/auto-add") -> None:
|
||||
"""
|
||||
Create the autoadd directory used to convert wbz track into szs
|
||||
:param auto_add_dir: autoadd directory
|
||||
"""
|
||||
if os.path.exists(auto_add_dir): shutil.rmtree(auto_add_dir)
|
||||
if not os.path.exists(self.path + "/tmp/"): os.makedirs(self.path + "/tmp/")
|
||||
wszst.autoadd(self.path, get_nodir(self.path) + "/tmp/auto-add/")
|
||||
shutil.move(self.path + "/tmp/auto-add/", auto_add_dir)
|
||||
shutil.rmtree(self.path + "/tmp/")
|
||||
|
||||
def patch_bmg(self, gamefile: str): # gamefile est le fichier .szs trouvé dans le /files/Scene/UI/ du jeu
|
||||
def patch_bmg(self, gamefile: str) -> None:
|
||||
"""
|
||||
Patch bmg file (text file)
|
||||
:param gamefile: an .szs file where file will be patched
|
||||
"""
|
||||
NINTENDO_CWF_REPLACE = "Wiimmfi"
|
||||
MAINMENU_REPLACE = f"MKWFaraphel {self.ctconfig.version}"
|
||||
menu_replacement = {
|
||||
|
@ -297,6 +339,9 @@ class Game:
|
|||
|
||||
@in_thread
|
||||
def patch_file(self):
|
||||
"""
|
||||
Prepare all files to install the mod (track, bmg text, descriptive image, ...)
|
||||
"""
|
||||
try:
|
||||
if not (os.path.exists("./file/Track-WU8/")): os.makedirs("./file/Track-WU8/")
|
||||
with open("./convert_file.json") as f:
|
||||
|
@ -329,13 +374,24 @@ class Game:
|
|||
finally:
|
||||
self.gui.progress(show=False)
|
||||
|
||||
def patch_image(self, fc):
|
||||
def patch_image(self, fc) -> None:
|
||||
"""
|
||||
Convert .png image into the format wrote in convert_file
|
||||
:param fc:
|
||||
:return:
|
||||
"""
|
||||
for i, file in enumerate(fc["img"]):
|
||||
self.gui.progress(statut=self.gui.translate("Converting images") + f"\n({i + 1}/{len(fc['img'])}) {file}",
|
||||
add=1)
|
||||
wszst.img_encode("./file/" + file, fc["img"][file])
|
||||
|
||||
def patch_img_desc(self, img_desc_path: str = "./file/img_desc", dest_dir: str = "./file"):
|
||||
def patch_img_desc(self, img_desc_path: str = "./file/img_desc", dest_dir: str = "./file") -> None:
|
||||
"""
|
||||
patch descriptive image used when the game boot
|
||||
:param img_desc_path: directory where original part of the image are stored
|
||||
:param dest_dir: directory where patched image will be saved
|
||||
:return:
|
||||
"""
|
||||
il = Image.open(img_desc_path + "/illustration.png")
|
||||
il_16_9 = il.resize((832, 456))
|
||||
il_4_3 = il.resize((608, 456))
|
||||
|
@ -355,12 +411,21 @@ class Game:
|
|||
new_4_3.paste(img_lang_4_3, (0, 0), img_lang_4_3)
|
||||
new_4_3.save(dest_dir + f"/strapA_608x456{get_filename(get_nodir(file_lang))}.png")
|
||||
|
||||
def patch_tracks(self):
|
||||
def patch_tracks(self) -> int:
|
||||
"""
|
||||
Download track's wu8 file and convert them to szs
|
||||
:return: 0 if no error occured
|
||||
"""
|
||||
max_process = self.gui.intvar_process_track.get()
|
||||
thread_list = {}
|
||||
error_count, error_max = 0, 3
|
||||
|
||||
def add_process(track):
|
||||
def add_process(track) -> int:
|
||||
"""
|
||||
a "single thread" to download, check sha1 and convert a track
|
||||
:param track: the track that will be patched
|
||||
:return: 0 if no error occured
|
||||
"""
|
||||
nonlocal error_count, error_max, thread_list
|
||||
|
||||
for _track in [track.file_szs, track.file_wu8]:
|
||||
|
@ -370,21 +435,21 @@ class Game:
|
|||
|
||||
if not self.gui.boolvar_disable_download.get():
|
||||
while True:
|
||||
download_returncode = track.download_wu8(
|
||||
GITHUB_DEV_BRANCH if self.gui.is_dev_version else GITHUB_MASTER_BRANCH)
|
||||
if download_returncode == -1: # can't download
|
||||
error_count += 1
|
||||
if error_count > error_max: # Too much track wasn't correctly converted
|
||||
messagebox.showerror(
|
||||
self.gui.translate("Error"),
|
||||
self.gui.translate("Too much tracks had a download issue."))
|
||||
raise TooMuchDownloadFailed()
|
||||
else:
|
||||
messagebox.showwarning(self.gui.translate("Warning"),
|
||||
self.gui.translate("Can't download this track !",
|
||||
f" ({error_count} / {error_max})"))
|
||||
elif download_returncode == 2:
|
||||
break # if download is disabled, do not check sha1
|
||||
download_returncode = 0
|
||||
if not os.path.exists(track.file_wu8):
|
||||
download_returncode = track.download_wu8(
|
||||
GITHUB_DEV_BRANCH if self.gui.is_dev_version else GITHUB_MASTER_BRANCH)
|
||||
if download_returncode == -1: # can't download
|
||||
error_count += 1
|
||||
if error_count > error_max: # Too much track wasn't correctly converted
|
||||
messagebox.showerror(
|
||||
self.gui.translate("Error"),
|
||||
self.gui.translate("Too much tracks had a download issue."))
|
||||
raise TooMuchDownloadFailed()
|
||||
else:
|
||||
messagebox.showwarning(self.gui.translate("Warning"),
|
||||
self.gui.translate("Can't download this track !",
|
||||
f" ({error_count} / {error_max})"))
|
||||
|
||||
if track.sha1:
|
||||
if not self.gui.boolvar_dont_check_track_sha1.get():
|
||||
|
@ -399,7 +464,7 @@ class Game:
|
|||
|
||||
break
|
||||
|
||||
if not (os.path.exists(track.file_szs)) or download_returncode == 3:
|
||||
if not os.path.exists(track.file_szs) or download_returncode == 3:
|
||||
# returncode 3 is track has been updated
|
||||
if os.path.exists(track.file_wu8):
|
||||
track.convert_wu8_to_szs()
|
||||
|
@ -411,29 +476,17 @@ class Game:
|
|||
os.remove(track.file_wu8)
|
||||
return 0
|
||||
|
||||
def clean_process():
|
||||
def clean_process() -> int:
|
||||
"""
|
||||
Check if a track conversion ended, and remove them from thread_list
|
||||
:return: 0 if thread_list is empty, else 1
|
||||
"""
|
||||
nonlocal error_count, error_max, thread_list
|
||||
|
||||
for track_key, thread in thread_list.copy().items():
|
||||
if not thread.is_alive(): # if conversion ended
|
||||
thread_list.pop(track_key)
|
||||
"""stderr = thread.stderr.read()
|
||||
if b"wszst: ERROR" in stderr: # Error occured
|
||||
os.remove(track.file_szs)
|
||||
error_count += 1
|
||||
if error_count > error_max: # Too much track wasn't correctly converted
|
||||
messagebox.showerror(
|
||||
self.gui.translate("Error"),
|
||||
self.gui.translate("Too much track had a conversion issue."))
|
||||
raise CantConvertTrack()
|
||||
else: # if the error max hasn't been reach
|
||||
messagebox.showwarning(
|
||||
self.gui.translate("Warning"),
|
||||
self.gui.translate("The track", " ", track.file_wu8,
|
||||
"do not have been properly converted.",
|
||||
f" ({error_count} / {error_max})"))
|
||||
else:
|
||||
if self.gui.boolvar_del_track_after_conv.get(): os.remove(track.file_wu8)"""
|
||||
if self.gui.boolvar_del_track_after_conv.get(): os.remove(track.file_wu8)
|
||||
if not (any(thread_list.values())): return 1 # if there is no more process
|
||||
|
||||
if len(thread_list): return 1
|
||||
|
@ -441,7 +494,7 @@ class Game:
|
|||
|
||||
total_track = len(self.ctconfig.all_tracks)
|
||||
for i, track in enumerate(self.ctconfig.all_tracks):
|
||||
while True:
|
||||
while error_count <= error_max:
|
||||
if len(thread_list) < max_process:
|
||||
track_name = track.get_track_name()
|
||||
thread_list[track_name] = Thread(target=add_process, args=[track])
|
||||
|
|
|
@ -20,6 +20,9 @@ def restart():
|
|||
|
||||
class Gui:
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize program Gui
|
||||
"""
|
||||
self.root = Tk()
|
||||
|
||||
self.option = Option()
|
||||
|
@ -156,7 +159,10 @@ class Gui:
|
|||
self.progressbar = ttk.Progressbar(self.root)
|
||||
self.progresslabel = Label(self.root)
|
||||
|
||||
def check_update(self):
|
||||
def check_update(self) -> None:
|
||||
"""
|
||||
Check if an update is available
|
||||
"""
|
||||
try:
|
||||
gitversion = requests.get(VERSION_FILE_URL, allow_redirects=True).json()
|
||||
with open("./version", "rb") as f:
|
||||
|
@ -200,13 +206,26 @@ class Gui:
|
|||
except:
|
||||
self.log_error()
|
||||
|
||||
def log_error(self):
|
||||
def log_error(self) -> None:
|
||||
"""
|
||||
When an error occur, will show it in a messagebox and write it in error.log
|
||||
"""
|
||||
error = traceback.format_exc()
|
||||
with open("./error.log", "a") as f:
|
||||
f.write(f"---\n{error}\n")
|
||||
messagebox.showerror(self.translate("Error"), self.translate("An error occured", " :", "\n", error, "\n\n"))
|
||||
|
||||
def progress(self, show=None, indeter=None, step=None, statut=None, max=None, add=None):
|
||||
def progress(self, show: bool = None, indeter: bool = None, step: int = None,
|
||||
statut: str = None, max: int = None, add: int = None) -> None:
|
||||
"""
|
||||
configure the progress bar shown when doing a task
|
||||
:param show: show or hide the progress bar
|
||||
:param indeter: if indeter, the progress bar will do a infinite loop animation
|
||||
:param step: set the progress of the bar
|
||||
:param statut: text shown under the progress bar
|
||||
:param max: set the maximum step
|
||||
:param add: add to step of the progress bar
|
||||
"""
|
||||
if indeter is True:
|
||||
self.progressbar.config(mode="indeterminate")
|
||||
self.progressbar.start(50)
|
||||
|
@ -229,7 +248,11 @@ class Gui:
|
|||
self.progressbar["value"] = 0
|
||||
if add: self.progressbar.step(add)
|
||||
|
||||
def state_button(self, enable=True):
|
||||
def state_button(self, enable: bool = True) -> None:
|
||||
"""
|
||||
used to enable or disable button when doing task
|
||||
:param enable: are the button enabled ?
|
||||
"""
|
||||
button = [
|
||||
self.button_game_extract,
|
||||
self.button_install_mod,
|
||||
|
@ -242,17 +265,18 @@ class Gui:
|
|||
else:
|
||||
widget.config(state=DISABLED)
|
||||
|
||||
def translate(self, *texts, lang=None):
|
||||
if lang is None:
|
||||
lang = self.stringvar_language.get()
|
||||
elif lang == "F":
|
||||
lang = "fr"
|
||||
elif lang == "G":
|
||||
lang = "ge"
|
||||
elif lang == "I":
|
||||
lang = "it"
|
||||
elif lang == "S":
|
||||
lang = "sp"
|
||||
def translate(self, *texts, lang: str = None) -> str:
|
||||
"""
|
||||
translate text into an another language in translation.json file
|
||||
:param texts: all text to convert
|
||||
:param lang: force a destination language to convert track
|
||||
:return: translated text
|
||||
"""
|
||||
if lang is None: lang = self.stringvar_language.get()
|
||||
elif lang == "F": lang = "fr"
|
||||
elif lang == "G": lang = "ge"
|
||||
elif lang == "I": lang = "it"
|
||||
elif lang == "S": lang = "sp"
|
||||
|
||||
if lang in translation_dict:
|
||||
_lang_trad = translation_dict[lang]
|
||||
|
|
|
@ -6,10 +6,31 @@ from . import wszst
|
|||
|
||||
|
||||
class Track:
|
||||
def __init__(self, name: str = "_", file_wu8: str = None, file_szs: str = None, prefix: str = None, suffix: str = None,
|
||||
def __init__(self, name: str = "_", prefix: str = None, suffix: str = None,
|
||||
author="Nintendo", special="T11", music="T11", new=True, sha1: str = None, since_version: str = None,
|
||||
score: int = 0, warning: int = 0, note: str = "", track_wu8_dir: str = "./file/Track-WU8/",
|
||||
track_szs_dir: str = "./file/Track/", *args, **kwargs):
|
||||
"""
|
||||
Track class
|
||||
:param name: track name
|
||||
:param file_wu8: path to its wu8 file
|
||||
:param file_szs: path to its szs file
|
||||
:param prefix: track prefix (often original console or game)
|
||||
:param suffix: track suffix (often for variation like Boost or Night)
|
||||
:param author: track creator
|
||||
:param special: track special slot
|
||||
:param music: track music slot
|
||||
:param new: is the track original or from an older game
|
||||
:param sha1: track sha1
|
||||
:param since_version: since when version did the track got added to the mod
|
||||
:param score: what it the score of the track
|
||||
:param warning: what is the warn level of the track (0 = none, 1 = minor bug, 2 = major bug)
|
||||
:param note: note about the track
|
||||
:param track_wu8_dir: where is stored the track wu8
|
||||
:param track_szs_dir: where is stored the track szs
|
||||
:param args: /
|
||||
:param kwargs: /
|
||||
"""
|
||||
|
||||
self.name = name # Track name
|
||||
self.prefix = prefix # Prefix, often used for game or original console like Wii U, DS, ...
|
||||
|
@ -28,18 +49,35 @@ class Track:
|
|||
self.file_wu8 = f"{track_wu8_dir}/{self.get_track_name()}.wu8"
|
||||
self.file_szs = f"{track_szs_dir}/{self.get_track_name()}.szs"
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
track representation when printed
|
||||
:return: track information
|
||||
"""
|
||||
return f"{self.get_track_name()} sha1={self.sha1} score={self.score}"
|
||||
|
||||
def check_sha1(self):
|
||||
def check_sha1(self) -> int:
|
||||
"""
|
||||
check if track wu8's sha1 is correct
|
||||
:return: 0 if yes, -1 if no
|
||||
"""
|
||||
ws = wszst.sha1(self.file_wu8)
|
||||
if wszst.sha1(self.file_wu8) == self.sha1: return 0
|
||||
else: return -1
|
||||
|
||||
def convert_wu8_to_szs(self):
|
||||
def convert_wu8_to_szs(self) -> str:
|
||||
"""
|
||||
convert track to szs
|
||||
:return: path to szs track
|
||||
"""
|
||||
return wszst.normalize(src_file=self.file_wu8)
|
||||
|
||||
def download_wu8(self, github_content_root: str):
|
||||
def download_wu8(self, github_content_root: str) -> int:
|
||||
"""
|
||||
download track wu8 from github
|
||||
:param github_content_root: url to github project root
|
||||
:return: 0 if correctly downloaded, 1 if no need to download, 3 if track size is incorrect, -1 if error
|
||||
"""
|
||||
returncode = 0
|
||||
|
||||
dl = requests.get(github_content_root + self.file_wu8, allow_redirects=True, stream=True)
|
||||
|
@ -60,8 +98,9 @@ class Track:
|
|||
print(f"error {dl.status_code} {self.file_wu8}")
|
||||
return -1
|
||||
|
||||
def get_ctfile(self, race=False, *args, **kwargs):
|
||||
def get_ctfile(self, race=False, *args, **kwargs) -> str:
|
||||
"""
|
||||
get ctfile text to create CTFILE.txt and RCTFILE.txt
|
||||
:param race: is it a text used for Race_*.szs ?
|
||||
:return: ctfile definition for the track
|
||||
"""
|
||||
|
@ -84,8 +123,9 @@ class Track:
|
|||
|
||||
return ctfile_text
|
||||
|
||||
def get_track_formatted_name(self, highlight_version: str = None):
|
||||
def get_track_formatted_name(self, highlight_version: str = None) -> str:
|
||||
"""
|
||||
get the track name with score, color, ...
|
||||
:param highlight_version: if a specific version need to be highlighted.
|
||||
:return: the name of the track with colored prefix, suffix
|
||||
"""
|
||||
|
@ -112,14 +152,22 @@ class Track:
|
|||
name = name.replace("_", " ")
|
||||
return name
|
||||
|
||||
def get_track_name(self):
|
||||
def get_track_name(self) -> str:
|
||||
"""
|
||||
get the track name without score, color...
|
||||
:return: track name
|
||||
"""
|
||||
prefix = (self.prefix + " ") if self.prefix else ""
|
||||
suffix = (" (" + self.suffix + ")") if self.suffix else ""
|
||||
|
||||
name = (prefix + self.name + suffix)
|
||||
return name
|
||||
|
||||
def load_from_json(self, track_json: dict):
|
||||
def load_from_json(self, track_json: dict) -> None:
|
||||
"""
|
||||
load the track from a dictionary
|
||||
:param track_json: track's dictionnary
|
||||
"""
|
||||
for key, value in track_json.items(): # load all value in the json as class attribute
|
||||
setattr(self, key, value)
|
||||
|
||||
|
|
Loading…
Reference in a new issue