Added docstring and more type hint tp Game, Gui and Track to make the code more understandable

This commit is contained in:
raphael60650 2021-07-25 14:55:19 +02:00
parent ee8fea9c5f
commit a6ec86c61d
3 changed files with 197 additions and 72 deletions

View file

@ -1,4 +1,4 @@
from tkinter import messagebox from tkinter import messagebox, StringVar, BooleanVar, IntVar
from PIL import Image from PIL import Image
import shutil import shutil
import glob import glob
@ -40,8 +40,32 @@ class CantConvertTrack(Exception):
super().__init__("Can't convert track, check if download are enabled.") 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: class Game:
def __init__(self, path: str = "", region_ID: str = "P", game_ID: str = "RMCP01", gui=None): 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() if not os.path.exists(path) and path: raise InvalidGamePath()
self.extension = None self.extension = None
self.path = path self.path = path
@ -49,17 +73,21 @@ class Game:
self.region = region_id_to_name[region_ID] self.region = region_id_to_name[region_ID]
self.region_ID = region_ID self.region_ID = region_ID
self.game_ID = game_ID self.game_ID = game_ID
self.gui = gui self.gui = gui if gui else NoGui
self.ctconfig = CT_Config(gui=gui) 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.extension = get_extension(path).upper()
self.path = path 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, ...) :param format: game format (ISO, WBFS, ...)
:return: converted game path
""" """
if format in ["ISO", "WBFS", "CISO"]: if format in ["ISO", "WBFS", "CISO"]:
path_game_format: str = os.path.realpath(self.path + "/../MKWFaraphel." + format.lower()) 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) 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}") 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": if self.extension == "DOL":
self.path = os.path.realpath(self.path + "/../../") # main.dol is in PATH/sys/, so go back 2 dir upper 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 @in_thread
def install_mod(self): def install_mod(self):
"""
Patch the game to install the mod
"""
try: try:
with open("./fs.json") as f: with open("./fs.json") as f:
fs = json.load(f) fs = json.load(f)
@ -204,14 +238,22 @@ class Game:
finally: finally:
self.gui.progress(show=False) 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 os.path.exists(auto_add_dir): shutil.rmtree(auto_add_dir)
if not os.path.exists(self.path + "/tmp/"): os.makedirs(self.path + "/tmp/") if not os.path.exists(self.path + "/tmp/"): os.makedirs(self.path + "/tmp/")
wszst.autoadd(self.path, get_nodir(self.path) + "/tmp/auto-add/") wszst.autoadd(self.path, get_nodir(self.path) + "/tmp/auto-add/")
shutil.move(self.path + "/tmp/auto-add/", auto_add_dir) shutil.move(self.path + "/tmp/auto-add/", auto_add_dir)
shutil.rmtree(self.path + "/tmp/") 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" NINTENDO_CWF_REPLACE = "Wiimmfi"
MAINMENU_REPLACE = f"MKWFaraphel {self.ctconfig.version}" MAINMENU_REPLACE = f"MKWFaraphel {self.ctconfig.version}"
menu_replacement = { menu_replacement = {
@ -297,6 +339,9 @@ class Game:
@in_thread @in_thread
def patch_file(self): def patch_file(self):
"""
Prepare all files to install the mod (track, bmg text, descriptive image, ...)
"""
try: try:
if not (os.path.exists("./file/Track-WU8/")): os.makedirs("./file/Track-WU8/") if not (os.path.exists("./file/Track-WU8/")): os.makedirs("./file/Track-WU8/")
with open("./convert_file.json") as f: with open("./convert_file.json") as f:
@ -329,13 +374,24 @@ class Game:
finally: finally:
self.gui.progress(show=False) 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"]): for i, file in enumerate(fc["img"]):
self.gui.progress(statut=self.gui.translate("Converting images") + f"\n({i + 1}/{len(fc['img'])}) {file}", self.gui.progress(statut=self.gui.translate("Converting images") + f"\n({i + 1}/{len(fc['img'])}) {file}",
add=1) add=1)
wszst.img_encode("./file/" + file, fc["img"][file]) 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 = Image.open(img_desc_path + "/illustration.png")
il_16_9 = il.resize((832, 456)) il_16_9 = il.resize((832, 456))
il_4_3 = il.resize((608, 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.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") 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() max_process = self.gui.intvar_process_track.get()
thread_list = {} thread_list = {}
error_count, error_max = 0, 3 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 nonlocal error_count, error_max, thread_list
for _track in [track.file_szs, track.file_wu8]: for _track in [track.file_szs, track.file_wu8]:
@ -370,21 +435,21 @@ class Game:
if not self.gui.boolvar_disable_download.get(): if not self.gui.boolvar_disable_download.get():
while True: while True:
download_returncode = track.download_wu8( download_returncode = 0
GITHUB_DEV_BRANCH if self.gui.is_dev_version else GITHUB_MASTER_BRANCH) if not os.path.exists(track.file_wu8):
if download_returncode == -1: # can't download download_returncode = track.download_wu8(
error_count += 1 GITHUB_DEV_BRANCH if self.gui.is_dev_version else GITHUB_MASTER_BRANCH)
if error_count > error_max: # Too much track wasn't correctly converted if download_returncode == -1: # can't download
messagebox.showerror( error_count += 1
self.gui.translate("Error"), if error_count > error_max: # Too much track wasn't correctly converted
self.gui.translate("Too much tracks had a download issue.")) messagebox.showerror(
raise TooMuchDownloadFailed() self.gui.translate("Error"),
else: self.gui.translate("Too much tracks had a download issue."))
messagebox.showwarning(self.gui.translate("Warning"), raise TooMuchDownloadFailed()
self.gui.translate("Can't download this track !", else:
f" ({error_count} / {error_max})")) messagebox.showwarning(self.gui.translate("Warning"),
elif download_returncode == 2: self.gui.translate("Can't download this track !",
break # if download is disabled, do not check sha1 f" ({error_count} / {error_max})"))
if track.sha1: if track.sha1:
if not self.gui.boolvar_dont_check_track_sha1.get(): if not self.gui.boolvar_dont_check_track_sha1.get():
@ -399,7 +464,7 @@ class Game:
break 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 # returncode 3 is track has been updated
if os.path.exists(track.file_wu8): if os.path.exists(track.file_wu8):
track.convert_wu8_to_szs() track.convert_wu8_to_szs()
@ -411,29 +476,17 @@ class Game:
os.remove(track.file_wu8) os.remove(track.file_wu8)
return 0 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 nonlocal error_count, error_max, thread_list
for track_key, thread in thread_list.copy().items(): for track_key, thread in thread_list.copy().items():
if not thread.is_alive(): # if conversion ended if not thread.is_alive(): # if conversion ended
thread_list.pop(track_key) thread_list.pop(track_key)
"""stderr = thread.stderr.read() if self.gui.boolvar_del_track_after_conv.get(): os.remove(track.file_wu8)
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 not (any(thread_list.values())): return 1 # if there is no more process if not (any(thread_list.values())): return 1 # if there is no more process
if len(thread_list): return 1 if len(thread_list): return 1
@ -441,7 +494,7 @@ class Game:
total_track = len(self.ctconfig.all_tracks) total_track = len(self.ctconfig.all_tracks)
for i, track in enumerate(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: if len(thread_list) < max_process:
track_name = track.get_track_name() track_name = track.get_track_name()
thread_list[track_name] = Thread(target=add_process, args=[track]) thread_list[track_name] = Thread(target=add_process, args=[track])

View file

@ -20,6 +20,9 @@ def restart():
class Gui: class Gui:
def __init__(self): def __init__(self):
"""
Initialize program Gui
"""
self.root = Tk() self.root = Tk()
self.option = Option() self.option = Option()
@ -156,7 +159,10 @@ class Gui:
self.progressbar = ttk.Progressbar(self.root) self.progressbar = ttk.Progressbar(self.root)
self.progresslabel = Label(self.root) self.progresslabel = Label(self.root)
def check_update(self): def check_update(self) -> None:
"""
Check if an update is available
"""
try: try:
gitversion = requests.get(VERSION_FILE_URL, allow_redirects=True).json() gitversion = requests.get(VERSION_FILE_URL, allow_redirects=True).json()
with open("./version", "rb") as f: with open("./version", "rb") as f:
@ -200,13 +206,26 @@ class Gui:
except: except:
self.log_error() 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() error = traceback.format_exc()
with open("./error.log", "a") as f: with open("./error.log", "a") as f:
f.write(f"---\n{error}\n") f.write(f"---\n{error}\n")
messagebox.showerror(self.translate("Error"), self.translate("An error occured", " :", "\n", error, "\n\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: if indeter is True:
self.progressbar.config(mode="indeterminate") self.progressbar.config(mode="indeterminate")
self.progressbar.start(50) self.progressbar.start(50)
@ -229,7 +248,11 @@ class Gui:
self.progressbar["value"] = 0 self.progressbar["value"] = 0
if add: self.progressbar.step(add) 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 = [ button = [
self.button_game_extract, self.button_game_extract,
self.button_install_mod, self.button_install_mod,
@ -242,17 +265,18 @@ class Gui:
else: else:
widget.config(state=DISABLED) widget.config(state=DISABLED)
def translate(self, *texts, lang=None): def translate(self, *texts, lang: str = None) -> str:
if lang is None: """
lang = self.stringvar_language.get() translate text into an another language in translation.json file
elif lang == "F": :param texts: all text to convert
lang = "fr" :param lang: force a destination language to convert track
elif lang == "G": :return: translated text
lang = "ge" """
elif lang == "I": if lang is None: lang = self.stringvar_language.get()
lang = "it" elif lang == "F": lang = "fr"
elif lang == "S": elif lang == "G": lang = "ge"
lang = "sp" elif lang == "I": lang = "it"
elif lang == "S": lang = "sp"
if lang in translation_dict: if lang in translation_dict:
_lang_trad = translation_dict[lang] _lang_trad = translation_dict[lang]

View file

@ -6,10 +6,31 @@ from . import wszst
class Track: 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, 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/", score: int = 0, warning: int = 0, note: str = "", track_wu8_dir: str = "./file/Track-WU8/",
track_szs_dir: str = "./file/Track/", *args, **kwargs): 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.name = name # Track name
self.prefix = prefix # Prefix, often used for game or original console like Wii U, DS, ... 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_wu8 = f"{track_wu8_dir}/{self.get_track_name()}.wu8"
self.file_szs = f"{track_szs_dir}/{self.get_track_name()}.szs" 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}" 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) ws = wszst.sha1(self.file_wu8)
if wszst.sha1(self.file_wu8) == self.sha1: return 0 if wszst.sha1(self.file_wu8) == self.sha1: return 0
else: return -1 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) 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 returncode = 0
dl = requests.get(github_content_root + self.file_wu8, allow_redirects=True, stream=True) 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}") print(f"error {dl.status_code} {self.file_wu8}")
return -1 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 ? :param race: is it a text used for Race_*.szs ?
:return: ctfile definition for the track :return: ctfile definition for the track
""" """
@ -84,8 +123,9 @@ class Track:
return ctfile_text 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. :param highlight_version: if a specific version need to be highlighted.
:return: the name of the track with colored prefix, suffix :return: the name of the track with colored prefix, suffix
""" """
@ -112,14 +152,22 @@ class Track:
name = name.replace("_", " ") name = name.replace("_", " ")
return name 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 "" prefix = (self.prefix + " ") if self.prefix else ""
suffix = (" (" + self.suffix + ")") if self.suffix else "" suffix = (" (" + self.suffix + ")") if self.suffix else ""
name = (prefix + self.name + suffix) name = (prefix + self.name + suffix)
return name 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 for key, value in track_json.items(): # load all value in the json as class attribute
setattr(self, key, value) setattr(self, key, value)