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
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])
@ -455,4 +508,4 @@ class Game:
while clean_process() != 1:
pass # End the process if all process ended
return 0
return 0

View file

@ -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]

View file

@ -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)