readded a better track selection filter (advanced menu), fixed an issue with tracks always renormalizing, added a default sort in ct_config, added a Common class to reunite all component with more readability and less weird code, splited the Gui into a directory

This commit is contained in:
Faraphel 2022-01-25 12:59:51 +01:00
parent 85e36c461c
commit b3d5af63ed
10 changed files with 463 additions and 177 deletions

View file

@ -1804,6 +1804,7 @@
]
}
],
"default_sort": "name",
"tracks_list":[
{
"name":"4IT Clown's Road",

View file

@ -1,4 +1,4 @@
from source.Gui import Gui
from source.Common import Common
gui = Gui()
gui.root.mainloop()
common = Common()
common.mainloop()

View file

@ -9,10 +9,10 @@ from source.Track import Track, get_trackdata_from_json
class CT_Config:
def __init__(self, version: str = None, name: str = None, nickname: str = None,
game_variant: str = "01", gui=None, region: int = None, cheat_region: int = None,
game_variant: str = "01", region: int = None, cheat_region: int = None,
tags_color: dict = None, prefix_list: list = None, suffix_list: list = None,
tag_retro: str = "Retro", default_track: Track = None, pack_path: str = "",
file_process: dict = None, file_structure: dict = None):
file_process: dict = None, file_structure: dict = None, default_sort: str = "name"):
self.version = version
self.name = name
@ -23,7 +23,6 @@ class CT_Config:
self.ordered_cups = []
self.unordered_tracks = []
self.gui = gui
self.tags_color = tags_color if tags_color else {}
self.prefix_list = prefix_list if tags_color else []
@ -32,10 +31,15 @@ class CT_Config:
self.default_track = default_track
self.pack_path = pack_path
self.sort_track_attr = default_sort
self.file_process = file_process
self.file_structure = file_structure
self.filter_track_selection = lambda track: True
self.filter_track_highlight = lambda track: False
self.filter_track_random_new = lambda track: getattr(track, "new", False)
def add_ordered_cup(self, cup: Cup) -> None:
"""
add a cup to the config
@ -53,9 +57,11 @@ class CT_Config:
def unordered_tracks_to_cup(self):
track_in_cup: int = 4
for cup_id, track_id in enumerate(range(0, len(self.unordered_tracks), track_in_cup), start=1):
track_selection = list(filter(self.filter_track_selection, self.unordered_tracks))
for cup_id, track_id in enumerate(range(0, len(track_selection), track_in_cup), start=1):
cup = Cup(id=cup_id, name=f"CT{cup_id}")
for index, track in enumerate(self.unordered_tracks[track_id:track_id + track_in_cup]):
for index, track in enumerate(track_selection[track_id:track_id + track_in_cup]):
cup.tracks[index] = track
yield cup
@ -77,8 +83,13 @@ class CT_Config:
ctfile.write(header); rctfile.write(header)
# all cups
kwargs = {
"filter_highlight": self.filter_track_highlight,
"filter_random_new": self.filter_track_random_new,
"ct_config": self
}
for cup in self.get_all_cups():
kwargs = {"highlight_version": highlight_version, "ct_config": self}
ctfile.write(cup.get_ctfile(race=False, **kwargs))
rctfile.write(cup.get_ctfile(race=True, **kwargs))
@ -183,8 +194,9 @@ class CT_Config:
self.version = ctconfig_json.get("version")
if "name" in ctconfig_json: self.name = ctconfig_json["name"]
self.nickname = ctconfig_json["nickname"] if "nickname" in ctconfig_json else self.name
if "game_variant" in ctconfig_json: self.game_variant = ctconfig_json["game_variant"]
if "default_sort" in ctconfig_json: self.default_sort = ctconfig_json["default_sort"]
self.nickname = ctconfig_json["nickname"] if "nickname" in ctconfig_json else self.name
for param in ["region", "cheat_region", "tags_color", "prefix_list", "suffix_list", "tag_retro"]:
setattr(self, param, ctconfig_json.get(param))

21
source/Common.py Normal file
View file

@ -0,0 +1,21 @@
from source.CT_Config import CT_Config
from source.Option import Option
from source.Game import Game
from source.Gui.Main import Main
from source.Gui.TrackSelection import TrackSelection
class Common:
def __init__(self):
"""
Common allow to store multiple object that need each other and still make the code readable enough without
having to access an object with some obscure way
"""
self.option = Option().load_from_file("./option.json")
self.ct_config = CT_Config()
self.game = Game(common=self)
self.gui_main = Main(common=self)
def show_gui_track_configuration(self): TrackSelection(common=self)
def mainloop(self): self.gui_main.mainloop()

View file

@ -4,53 +4,19 @@ import shutil
import glob
import json
from source.CT_Config import CT_Config
from source.definition import *
from source.wszst import *
from source.Error import *
class NoGui:
"""
'fake' gui if no gui are used for compatibility.
"""
class NoButton:
def grid(self, *args, **kwargs): pass
def config(self, *args, **kwargs): pass
class NoVariable:
def __init__(self, value=None):
self.value = None
def set(self, value):
self.value = value
def get(self):
return self.value
def progress(*args, **kwargs): print(args, kwargs)
def translate(*args, **kwargs): return ""
def log_error(*args, **kwargs): print(args, kwargs)
is_dev_version = False
button_install_mod = NoButton()
stringvar_game_format = NoVariable()
intvar_process_track = NoVariable()
boolvar_dont_check_track_sha1 = NoVariable()
class Game:
def __init__(self, path: str = "", region_ID: str = "P", game_ID: str = "RMCP01", gui=None):
def __init__(self, common, path: str = "", region_ID: str = "P", game_ID: str = "RMCP01"):
"""
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
:param common: common class to access all other element
"""
if not os.path.exists(path) and path: raise InvalidGamePath()
self.extension = None
@ -59,8 +25,7 @@ class Game:
self.region = region_id_to_name[region_ID]
self.region_ID = region_ID
self.game_ID = game_ID
self.gui = gui if gui else NoGui
self.ctconfig = CT_Config(gui=gui)
self.common = common
def set_path(self, path: str) -> None:
"""
@ -76,17 +41,17 @@ class Game:
:param format: game format (ISO, WBFS, ...)
"""
if format in ["ISO", "WBFS", "CISO"]:
path_game_format: str = os.path.realpath(self.path + f"/../{self.ctconfig.nickname} v{self.ctconfig.version}." + format.lower())
path_game_format: str = os.path.realpath(self.path + f"/../{self.common.ct_config.nickname} v{self.common.ct_config.version}." + format.lower())
wit.copy(src_path=self.path, dst_path=path_game_format, format=format)
shutil.rmtree(self.path)
self.path = path_game_format
self.gui.progress(statut=self.gui.translate("Changing game's ID"), add=1)
self.common.gui_main.progress(statut=self.common.gui_main.translate("Changing game's ID"), add=1)
wit.edit(
file=self.path,
region_ID=self.region_ID,
game_variant=self.ctconfig.game_variant,
name=f"{self.ctconfig.name} {self.ctconfig.version}"
game_variant=self.common.ct_config.game_variant,
name=f"{self.common.ct_config.name} {self.common.ct_config.version}"
)
def extract(self) -> None:
@ -100,7 +65,7 @@ class Game:
# Fiding a directory name that doesn't already exist
path_dir = get_next_available_dir(
parent_dir=self.path + f"/../",
dir_name=f"{self.ctconfig.nickname} v{self.ctconfig.version}"
dir_name=f"{self.common.ct_config.nickname} v{self.common.ct_config.version}"
)
wit.extract(file=self.path, dst_dir=path_dir)
@ -128,7 +93,7 @@ class Game:
count all the step patching subfile will take (for the progress bar)
:return: number of step estimated
"""
with open(f"{self.ctconfig.pack_path}/file_structure.json") as f:
with open(f"{self.common.ct_config.pack_path}/file_structure.json") as f:
fs = json.load(f)
# This part is used to estimate the max_step
@ -160,11 +125,11 @@ class Game:
"""
patch subfile as indicated in the file_structure.json file (for file structure)
"""
with open(f"{self.ctconfig.pack_path}/file_structure.json") as f:
with open(f"{self.common.ct_config.pack_path}/file_structure.json") as f:
fs = json.load(f)
extracted_file = []
self.gui.progress(show=True, indeter=False, statut=self.gui.translate("Modifying subfile..."), add=1)
self.common.gui_main.progress(show=True, indeter=False, statut=self.common.gui_main.translate("Modifying subfile..."), add=1)
def replace_file(path, file, subpath="/") -> None:
"""
@ -173,10 +138,10 @@ class Game:
:param file: file to replace
:param subpath: directory between .szs file and file inside to replace
"""
self.gui.progress(statut=self.gui.translate("Editing", "\n", get_nodir(path)), add=1)
self.common.gui_main.progress(statut=self.common.gui_main.translate("Editing", "\n", get_nodir(path)), add=1)
extension = get_extension(path)
source_file = f"{self.ctconfig.pack_path}/file/{file}"
source_file = f"{self.common.ct_config.pack_path}/file/{file}"
dest_file = path
if extension == "szs":
@ -205,7 +170,7 @@ class Game:
for ffp in fs[fp][nf]: replace_file(path=f, subpath=nf, file=ffp)
for file in extracted_file:
self.gui.progress(statut=self.gui.translate("Recompilating", "\n", get_nodir(file)), add=1)
self.common.gui_main.progress(statut=self.common.gui_main.translate("Recompilating", "\n", get_nodir(file)), add=1)
szs.create(file=file)
shutil.rmtree(file + ".d", ignore_errors=True)
@ -213,9 +178,9 @@ class Game:
"""
copy MyStuff directory into the game *before* patching the game
"""
self.gui.progress(show=True, indeter=False, statut=self.gui.translate("Copying MyStuff..."), add=1)
self.common.gui_main.progress(show=True, indeter=False, statut=self.common.gui_main.translate("Copying MyStuff..."), add=1)
mystuff_folder = self.gui.stringvar_mystuff_folder.get()
mystuff_folder = self.common.gui_main.stringvar_mystuff_folder.get()
if mystuff_folder and mystuff_folder != "None":
# replace game's file by files with the same name in the MyStuff root
@ -241,27 +206,27 @@ class Game:
"""
patch the main.dol file to allow the addition of LECODE.bin file
"""
self.gui.progress(statut=self.gui.translate("Patch main.dol"), add=1)
self.common.gui_main.progress(statut=self.common.gui_main.translate("Patch main.dol"), add=1)
region_id = self.ctconfig.region if self.gui.is_using_official_config() else self.ctconfig.cheat_region
region_id = self.common.ct_config.region if self.common.gui_main.is_using_official_config() else self.common.ct_config.cheat_region
wstrt.patch(path=self.path, region_id=region_id)
def install_patch_lecode(self) -> None:
"""
configure and add the LECODE.bin file to the mod
"""
self.gui.progress(statut=self.gui.translate("Patch lecode.bin"), add=1)
self.common.gui_main.progress(statut=self.common.gui_main.translate("Patch lecode.bin"), add=1)
lpar_path = self.ctconfig.file_process["placement"].get("lpar_dir")
lpar_path = self.common.ct_config.file_process["placement"].get("lpar_dir")
if not lpar_path: f""
lpar_path = (
f"{self.ctconfig.pack_path}/file/{lpar_path}/"
f"lpar-{'debug' if self.gui.boolvar_use_debug_mode.get() else 'normal'}.txt"
f"{self.common.ct_config.pack_path}/file/{lpar_path}/"
f"lpar-{'debug' if self.common.gui_main.boolvar_use_debug_mode.get() else 'normal'}.txt"
)
lecode_file = self.ctconfig.file_process["placement"].get("lecode_bin_dir")
lecode_file = self.common.ct_config.file_process["placement"].get("lecode_bin_dir")
if not lecode_file: lecode_file = ""
lecode_file = f"{self.ctconfig.pack_path}/file/{lecode_file}/lecode-{self.region}.bin"
lecode_file = f"{self.common.ct_config.pack_path}/file/{lecode_file}/lecode-{self.region}.bin"
lec.patch(
lecode_file=lecode_file,
@ -277,8 +242,8 @@ class Game:
"""
convert the rom to the selected game format
"""
output_format = self.gui.stringvar_game_format.get()
self.gui.progress(statut=self.gui.translate("Converting to", " ", output_format), add=1)
output_format = self.common.gui_main.stringvar_game_format.get()
self.common.gui_main.progress(statut=self.common.gui_main.translate("Converting to", " ", output_format), add=1)
self.convert_to(output_format)
def install_mod(self) -> None:
@ -289,22 +254,22 @@ class Game:
max_step = 5 + self.count_patch_subfile_operation()
# PATCH main.dol and PATCH lecode.bin, converting, changing ID, copying MyStuff Folder
self.gui.progress(statut=self.gui.translate("Installing mod..."), max=max_step, step=0)
self.common.gui_main.progress(statut=self.common.gui_main.translate("Installing mod..."), max=max_step, step=0)
self.install_copy_mystuff()
self.install_patch_subfile()
self.install_patch_maindol()
self.install_patch_lecode()
self.install_convert_rom()
messagebox.showinfo(self.gui.translate("End"), self.gui.translate("The mod have been installed !"))
messagebox.showinfo(self.common.gui_main.translate("End"), self.common.gui_main.translate("The mod have been installed !"))
except Exception as e:
self.gui.log_error()
self.common.gui_main.log_error()
raise e
finally:
self.gui.progress(show=False)
self.gui.quit()
self.common.gui_main.progress(show=False)
self.common.gui_main.quit()
def patch_autoadd(self, auto_add_dir: str = "./file/auto-add") -> None:
"""
@ -337,15 +302,15 @@ class Game:
track_id = bmgtrack[start_track_id:start_track_id + 3]
if track_id[1] in "1234": # if the track is a original track from the wii
prefix = "Wii"
if prefix in self.ctconfig.tags_color:
prefix = "\\\\c{" + self.ctconfig.tags_color[prefix] + "}" + prefix + "\\\\c{off}"
if prefix in self.common.ct_config.tags_color:
prefix = "\\\\c{" + self.common.ct_config.tags_color[prefix] + "}" + prefix + "\\\\c{off}"
prefix += " "
elif track_id[1] in "5678": # if the track is a retro track from the original game
prefix, *track_name = track_name.split(" ")
track_name = " ".join(track_name)
if prefix in self.ctconfig.tags_color:
prefix = "\\\\c{" + self.ctconfig.tags_color[prefix] + "}" + prefix + "\\\\c{off}"
if prefix in self.common.ct_config.tags_color:
prefix = "\\\\c{" + self.common.ct_config.tags_color[prefix] + "}" + prefix + "\\\\c{off}"
prefix += " "
track_id = hex(bmgID_track_move[track_id])[2:]
@ -364,15 +329,15 @@ class Game:
"""
bmg_replacement = {
"MOD_NAME": self.ctconfig.name,
"MOD_NICKNAME": self.ctconfig.nickname,
"MOD_VERSION": self.ctconfig.version,
"MOD_CUSTOMIZED": "" if self.gui.is_using_official_config() else "(custom)",
"MOD_NAME": self.common.ct_config.name,
"MOD_NICKNAME": self.common.ct_config.nickname,
"MOD_VERSION": self.common.ct_config.version,
"MOD_CUSTOMIZED": "" if self.common.gui_main.is_using_official_config() else "(custom)",
"ONLINE_SERVICE": "Wiimmfi",
}
bmglang = gamefile[-len("E.txt"):-len(".txt")] # Langue du fichier
self.gui.progress(statut=self.gui.translate("Patching text", " ", bmglang), add=1)
self.common.gui_main.progress(statut=self.common.gui_main.translate("Patching text", " ", bmglang), add=1)
szs.extract(file=gamefile)
@ -401,7 +366,7 @@ class Game:
:param bmg_language: language of the bmg file
:return: the replaced bmg file
"""
with open(f"{self.ctconfig.pack_path}/file_process.json", encoding="utf8") as fp_file:
with open(f"{self.common.ct_config.pack_path}/file_process.json", encoding="utf8") as fp_file:
file_process = json.load(fp_file)
for bmg_process in file_process["bmg"]:
@ -440,8 +405,8 @@ class Game:
bmg.encode(file)
os.remove(file)
bmg_dir = self.ctconfig.file_process["placement"].get("bmg_patch_dir")
bmg_dir = f"{self.ctconfig.pack_path}/file/{bmg_dir if bmg_dir else ''}"
bmg_dir = self.common.ct_config.file_process["placement"].get("bmg_patch_dir")
bmg_dir = f"{self.common.ct_config.pack_path}/file/{bmg_dir if bmg_dir else ''}"
os.makedirs(get_dir(bmg_dir), exist_ok=True)
save_bmg(f"{bmg_dir}/Menu_{bmglang}.txt", process_bmg_replacement(bmgmenu, bmglang))
@ -457,18 +422,18 @@ class Game:
Prepare all files to install the mod (track, bmg text, descriptive image, ...)
"""
try:
os.makedirs(f"{self.ctconfig.pack_path}/file/Track-WU8/", exist_ok=True)
os.makedirs(f"{self.common.ct_config.pack_path}/file/Track-WU8/", exist_ok=True)
max_step = len(self.ctconfig.file_process["img_encode"]) + \
len(self.ctconfig.all_tracks) + \
max_step = len(self.common.ct_config.file_process["img_encode"]) + \
len(self.common.ct_config.all_tracks) + \
3 + \
len("EGFIS")
self.gui.progress(show=True, indeter=False, statut=self.gui.translate("Converting files"),
self.common.gui_main.progress(show=True, indeter=False, statut=self.common.gui_main.translate("Converting files"),
max=max_step, step=0)
self.gui.progress(statut=self.gui.translate("Configurating LE-CODE"), add=1)
self.ctconfig.create_ctfile(
highlight_version=self.gui.stringvar_mark_track_from_version.get(),
self.common.gui_main.progress(statut=self.common.gui_main.translate("Configurating LE-CODE"), add=1)
self.common.ct_config.create_ctfile(
highlight_version=self.common.gui_main.stringvar_mark_track_from_version.get(),
)
self.generate_cticons()
@ -479,37 +444,37 @@ class Game:
self.patch_tracks()
except Exception as e:
self.gui.log_error()
self.common.gui_main.log_error()
raise e
finally:
self.gui.progress(show=False)
self.common.gui_main.progress(show=False)
def generate_cticons(self):
file = self.ctconfig.file_process["placement"].get("ct_icons")
file = self.common.ct_config.file_process["placement"].get("ct_icons")
if not file: file = "ct_icons.tpl.png"
file = f"{self.ctconfig.pack_path}/file/{file}"
file = f"{self.common.ct_config.pack_path}/file/{file}"
os.makedirs(get_dir(file), exist_ok=True)
self.ctconfig.get_cticon().save(file)
self.common.ct_config.get_cticon().save(file)
def patch_image(self) -> None:
"""
Convert .png image into the format wrote in convert_file
"""
image_amount = len(self.ctconfig.file_process["img_encode"])
image_amount = len(self.common.ct_config.file_process["img_encode"])
for i, (file, data) in enumerate(self.ctconfig.file_process["img_encode"].items()):
self.gui.progress(
statut=self.gui.translate("Converting images") + f"\n({i + 1}/{image_amount}) {file}",
for i, (file, data) in enumerate(self.common.ct_config.file_process["img_encode"].items()):
self.common.gui_main.progress(
statut=self.common.gui_main.translate("Converting images") + f"\n({i + 1}/{image_amount}) {file}",
add=1
)
img.encode(
file=f"{self.ctconfig.pack_path}/file/{file}",
file=f"{self.common.ct_config.pack_path}/file/{file}",
format=data["format"],
dest_file=f"{self.ctconfig.pack_path}/file/{data['dest']}" if "dest" in data else None
dest_file=f"{self.common.ct_config.pack_path}/file/{data['dest']}" if "dest" in data else None
)
def generate_image(self, generator: dict) -> Image.Image:
@ -546,7 +511,7 @@ class Game:
tuple(layer["color"]) if "color" in layer else 0
)
if layer["type"] == "image":
layer_image = Image.open(f'{self.ctconfig.pack_path}/file/{layer["path"]}')
layer_image = Image.open(f'{self.common.ct_config.pack_path}/file/{layer["path"]}')
layer_image = layer_image.resize(get_layer_size(layer)).convert("RGBA")
image.paste(
layer_image,
@ -555,7 +520,7 @@ class Game:
)
if layer["type"] == "text":
font = ImageFont.truetype(
font=f'{self.ctconfig.pack_path}/file/{layer["font"]}' if "font" in layer else None,
font=f'{self.common.ct_config.pack_path}/file/{layer["font"]}' if "font" in layer else None,
size=int(layer["text_size"] * generator["height"]) if "text_size" in layer else 10,
)
draw.text(
@ -568,8 +533,8 @@ class Game:
return image
def generate_all_image(self) -> None:
for file, generator in self.ctconfig.file_process["img_generator"].items():
file = f"{self.ctconfig.pack_path}/file/{file}"
for file, generator in self.common.ct_config.file_process["img_generator"].items():
file = f"{self.common.ct_config.pack_path}/file/{file}"
os.makedirs(get_dir(file), exist_ok=True)
self.generate_image(generator).save(file)
@ -577,7 +542,7 @@ class Game:
"""
Download track's wu8 file and convert them to szs
"""
max_process = self.gui.intvar_process_track.get()
max_process = self.common.gui_main.intvar_process_track.get()
thread_list = {}
error_count, error_max = 0, 3
@ -607,16 +572,16 @@ class Game:
return bool(thread_list)
total_track = self.ctconfig.get_tracks_count()
self.gui.progress(max=total_track, indeter=False, show=True)
total_track = self.common.ct_config.get_tracks_count()
self.common.gui_main.progress(max=total_track, indeter=False, show=True)
for i, track in enumerate(self.ctconfig.get_tracks()):
for i, track in enumerate(self.common.ct_config.get_tracks()):
while error_count <= error_max:
if len(thread_list) < max_process:
thread_list[track.sha1] = Thread(target=add_process, args=[track])
thread_list[track.sha1].setDaemon(True)
thread_list[track.sha1].start()
self.gui.progress(statut=self.gui.translate("Converting tracks", f"\n({i + 1}/{total_track})\n",
self.common.gui_main.progress(statut=self.common.gui_main.translate("Converting tracks", f"\n({i + 1}/{total_track})\n",
"\n".join(thread_list.keys())), add=1)
break
clean_process()

View file

View file

@ -8,9 +8,7 @@ import zipfile
import glob
import json
from source.Game import Game, RomAlreadyPatched, InvalidGamePath, InvalidFormat
from source.Option import Option
from source.Error import *
from source.definition import *
@ -18,8 +16,8 @@ with open("./translation.json", encoding="utf-8") as f:
translation_dict = json.load(f)
class Gui:
def __init__(self) -> None:
class Main:
def __init__(self, common) -> None:
"""
Initialize program Gui
"""
@ -27,9 +25,7 @@ class Gui:
self.root.resizable(False, False)
self.root.iconbitmap(bitmap="./icon.ico")
self.option = Option().load_from_file("./option.json")
self.game = Game(gui=self)
self.common = common
self.menu_bar = None
self.available_packs = self.get_available_packs()
@ -42,10 +38,10 @@ class Gui:
self.is_dev_version = False # Is this installer version a dev ?
self.stringvar_ctconfig = StringVar(value=self.available_packs[0])
self.stringvar_language = StringVar(value=self.option.language)
self.stringvar_game_format = StringVar(value=self.option.format)
self.boolvar_dont_check_for_update = BooleanVar(value=self.option.dont_check_for_update)
self.intvar_process_track = IntVar(value=self.option.process_track)
self.stringvar_language = StringVar(value=self.common.option.language)
self.stringvar_game_format = StringVar(value=self.common.option.format)
self.boolvar_dont_check_for_update = BooleanVar(value=self.common.option.dont_check_for_update)
self.intvar_process_track = IntVar(value=self.common.option.process_track)
self.root.title(self.translate("MKWFaraphel Installer"))
@ -104,9 +100,9 @@ class Gui:
game_path = entry_game_path.get()
if not os.path.exists(game_path): raise InvalidGamePath
self.game.set_path(game_path)
self.common.game.set_path(game_path)
self.progress(show=True, indeter=True, statut=self.translate("Extracting the game..."))
self.game.extract()
self.common.game.extract()
except RomAlreadyPatched:
messagebox.showerror(self.translate("Error"), self.translate("This game is already modded"))
@ -126,8 +122,8 @@ class Gui:
@in_thread
def do_everything():
use_path()
self.game.patch_file()
self.game.install_mod()
self.common.game.patch_file()
self.common.game.install_mod()
self.button_do_everything = Button(
self.frame_game_path_action,
@ -152,13 +148,13 @@ class Gui:
label="Français",
variable=self.stringvar_language,
value="fr",
command=lambda: self.option.edit("language", "fr", need_restart=True)
command=lambda: self.common.option.edit("language", "fr", need_restart=True)
)
self.menu_language.add_radiobutton(
label="English",
variable=self.stringvar_language,
value="en",
command=lambda: self.option.edit("language", "en", need_restart=True)
command=lambda: self.common.option.edit("language", "en", need_restart=True)
)
# OUTPUT FORMAT MENU
@ -168,25 +164,25 @@ class Gui:
label=self.translate("FST (Directory)"),
variable=self.stringvar_game_format,
value="FST", command=lambda:
self.option.edit("format", "FST")
self.common.option.edit("format", "FST")
)
self.menu_format.add_radiobutton(
label="ISO",
variable=self.stringvar_game_format,
value="ISO",
command=lambda: self.option.edit("format", "ISO")
command=lambda: self.common.option.edit("format", "ISO")
)
self.menu_format.add_radiobutton(
label="CISO",
variable=self.stringvar_game_format,
value="CISO",
command=lambda: self.option.edit("format", "CISO")
command=lambda: self.common.option.edit("format", "CISO")
)
self.menu_format.add_radiobutton(
label="WBFS",
variable=self.stringvar_game_format,
value="WBFS",
command=lambda: self.option.edit("format", "WBFS")
command=lambda: self.common.option.edit("format", "WBFS")
)
# ADVANCED MENU
@ -196,7 +192,7 @@ class Gui:
self.menu_advanced.add_checkbutton(
label=self.translate("Don't check for update"),
variable=self.boolvar_dont_check_for_update,
command=lambda: self.option.edit(
command=lambda: self.common.option.edit(
"dont_check_for_update",
self.boolvar_dont_check_for_update
)
@ -215,29 +211,36 @@ class Gui:
self.menu_conv_process.add_radiobutton(
label=self.translate("1 ", "process"),
variable=self.intvar_process_track, value=1,
command=lambda: self.option.edit("process_track", 1)
command=lambda: self.common.option.edit("process_track", 1)
)
self.menu_conv_process.add_radiobutton(
label=self.translate("2 ", "process"),
variable=self.intvar_process_track, value=2,
command=lambda: self.option.edit("process_track", 2)
command=lambda: self.common.option.edit("process_track", 2)
)
self.menu_conv_process.add_radiobutton(
label=self.translate("4 ", "process"),
variable=self.intvar_process_track, value=4,
command=lambda: self.option.edit("process_track", 4)
command=lambda: self.common.option.edit("process_track", 4)
)
self.menu_conv_process.add_radiobutton(
label=self.translate("8 ", "process"),
variable=self.intvar_process_track, value=8,
command=lambda: self.option.edit("process_track", 8)
command=lambda: self.common.option.edit("process_track", 8)
)
## GAME PARAMETER
self.menu_advanced.add_separator()
self.menu_advanced.add_checkbutton(label=self.translate("Use debug mode"),
variable=self.boolvar_use_debug_mode)
self.menu_advanced.add_command(
label=self.translate("Change track configuration"),
command=self.common.show_gui_track_configuration
)
self.menu_advanced.add_checkbutton(
label=self.translate("Use debug mode"),
variable=self.boolvar_use_debug_mode
)
self.menu_mystuff = Menu(self.menu_advanced, tearoff=0)
self.menu_advanced.add_cascade(label=self.translate("MyStuff"), menu=self.menu_mystuff)
@ -270,7 +273,7 @@ class Gui:
self.menu_help.add_command(label="Discord", command=lambda: webbrowser.open(DISCORD_URL))
def reload_ctconfig(self) -> None:
self.game.ctconfig.load_ctconfig_file(
self.common.ct_config.load_ctconfig_file(
ctconfig_file=self.get_ctconfig_path_pack(self.stringvar_ctconfig.get())
)
@ -326,7 +329,7 @@ class Gui:
except requests.ConnectionError:
messagebox.showwarning(self.translate("Warning"),
self.translate("Can't connect to internet. Download will be disabled."))
self.option.disable_download = True
self.common.option.disable_download = True
except:
self.log_error()
@ -339,10 +342,10 @@ class Gui:
with open("./error.log", "a") as f:
f.write(
f"---\n"
f"For game version : {self.game.ctconfig.version}\n"
f"For game version : {self.common.ct_config.version}\n"
f"./file/ directory : {os.listdir('./file/')}\n"
f"ctconfig directory : {os.listdir(self.game.ctconfig.pack_path)}\n"
f"GAME/files/ information : {self.game.path, self.game.region}\n"
f"ctconfig directory : {os.listdir(self.common.ct_config.pack_path)}\n"
f"GAME/files/ information : {self.common.game.path, self.common.game.region}\n"
f"{error}\n"
)
messagebox.showerror(
@ -404,7 +407,7 @@ class Gui:
:param gamelang: force a destination language to convert track
:return: translated text
"""
lang = gamelang_to_lang.get(gamelang, self.stringvar_language.get())
lang = gamelang_to_lang.get(gamelang, self.common.option.language)
if lang not in translation_dict: return "".join(texts) # if no translation language is found
_lang_trad = translation_dict[lang]
@ -434,3 +437,6 @@ class Gui:
self.root.quit()
self.root.destroy()
sys.exit()
def mainloop(self) -> None:
self.root.mainloop()

3
source/Gui/SelectPack.py Normal file
View file

@ -0,0 +1,3 @@
class SelectPack:
def __init__(self):
pass

View file

@ -0,0 +1,283 @@
from tkinter import *
from tkinter import ttk
class Orderbox(Listbox):
def order_up(self, *args, **kwargs):
self.order_change(delta_start=1)
def order_down(self, *args, **kwargs):
self.order_change(delta_end=1)
def order_change(self, delta_start: int = 0, delta_end: int = 0):
selection = self.curselection()
if len(selection) < 1: return
index = selection[0]
values = self.get(index - delta_start, index + delta_end)
if len(values) < 2: return
self.delete(index - delta_start, index + delta_end)
self.insert(index - delta_start, *reversed(values))
self.selection_set(index - delta_start + delta_end)
class TrackSelection:
def __init__(self, common):
self.common = common
self.root = Toplevel(self.common.gui_main.root)
self.root.title("Track selection")
self.root.iconbitmap("./icon.ico")
self.root.resizable(False, False)
self.root.grab_set()
self.text_is_equal_to = "is equal to"
self.text_is_in = "is in"
self.text_is_between = "is between"
self.text_contains = "contains"
self.text_and = "and"
self.text_nand = "nand"
self.text_or = "or"
self.text_nor = "nor"
self.text_xor = "xor"
self.text_xnor = "xnor"
self.condition_link_end = "end"
track_filter_row_start = 10
self.condition_links = {
self.text_and: lambda a, b: lambda track: a(track) and b(track),
self.text_nand: lambda a, b: lambda track: not (a(track) and b(track)),
self.text_or: lambda a, b: lambda track: a(track) or b(track),
self.text_nor: lambda a, b: lambda track: not (a(track) or b(track)),
self.text_xor: lambda a, b: lambda track: a(track) != b(track),
self.text_xnor: lambda a, b: lambda track: a(track) == b(track),
self.condition_link_end: -1
}
def del_frame_track_filter(frames_filter: list, index: int = 0):
for elem in frames_filter[index:]: # remove all track filter after this one
elem["frame"].destroy()
del frames_filter[index:]
def add_frame_track_filter(root: Frame, frames_filter: list, index: int = 0):
frame = Frame(root)
frame.grid(row=index + track_filter_row_start, column=1, sticky="NEWS")
Label(frame, text="If track's").grid(row=1, column=1)
track_property = ttk.Combobox(frame, values=list(self.common.ct_config.get_all_track_possibilities().keys()))
track_property.current(0)
track_property.grid(row=1, column=2)
frame_equal = Frame(frame)
entry_equal = Entry(frame_equal, width=20)
entry_equal.grid(row=1, column=1)
entry_equal.insert(END, "value")
frame_in = Frame(frame)
entry_in = Entry(frame_in, width=30)
entry_in.grid(row=1, column=1)
entry_in.insert(END, "value1, value2, ...")
frame_between = Frame(frame)
entry_start = Entry(frame_between, width=10)
entry_start.grid(row=1, column=1)
entry_start.insert(END, "value1")
Label(frame_between, text="and").grid(row=1, column=2)
entry_end = Entry(frame_between, width=10)
entry_end.insert(END, "value2")
entry_end.grid(row=1, column=3)
frame_contains = Frame(frame)
entry_contains = Entry(frame_contains, width=20)
entry_contains.grid(row=1, column=1)
entry_contains.insert(END, "value")
condition_frames = {
self.text_is_equal_to: frame_equal,
self.text_is_in: frame_in,
self.text_is_between: frame_between,
self.text_contains: frame_contains,
}
def change_condition_type(event: Event = None):
condition = combobox_condition_type.get()
for frame in condition_frames.values(): frame.grid_forget()
condition_frames[condition].grid(row=1, column=10)
combobox_condition_type = ttk.Combobox(frame, values=list(condition_frames.keys()), width=10)
combobox_condition_type.current(0)
combobox_condition_type.bind("<<ComboboxSelected>>", change_condition_type)
change_condition_type()
combobox_condition_type.grid(row=1, column=3)
def change_condition_link(event: Event = None):
link = next_condition_link.get()
if link == self.condition_link_end:
del_frame_track_filter(frames_filter, index=index + 1)
else:
if frames_filter[-1]["frame"] == frame: # if this is the last filter available
add_frame_track_filter(root=root, frames_filter=frames_filter, index=index + 1)
next_condition_link = ttk.Combobox(frame, values=list(self.condition_links.keys()), width=10)
next_condition_link.bind("<<ComboboxSelected>>", change_condition_link)
next_condition_link.set(self.condition_link_end)
next_condition_link.grid(row=1, column=100)
frames_filter.append({
"frame": frame,
"track_property": track_property,
"condition_type": combobox_condition_type,
"value_equal": entry_equal,
"value_in": entry_in,
"value_between_start": entry_start,
"value_between_end": entry_end,
"value_contains": entry_contains,
"next_condition_link": next_condition_link
})
def get_change_enable_track_filter_func(root: [Frame, LabelFrame], frames_filter: list, variable_enable: BooleanVar):
def change_enable_track_filter(event: Event = None):
if variable_enable.get(): add_frame_track_filter(root=root, frames_filter=frames_filter)
else: del_frame_track_filter(frames_filter=frames_filter)
return change_enable_track_filter
self.track_sort = LabelFrame(self.root, text="Sort Track")
self.track_sort.grid(row=1, column=1, sticky="NEWS")
Label(self.track_sort, text="Sort track by : ").grid(row=1, column=1)
self.combobox_track_sort = ttk.Combobox(
self.track_sort,
values=list(self.common.ct_config.get_all_track_possibilities().keys())
)
self.combobox_track_sort.grid(row=1, column=2, sticky="NEWS")
self.combobox_track_sort.insert(END, self.common.ct_config.sort_track_attr)
self.track_filter = LabelFrame(self.root, text="Filter Track")
self.track_filter.grid(row=2, column=1, sticky="NEWS")
self.variable_enable_track_filter = BooleanVar(value=False)
self.frames_track_filter = []
self.checkbutton_track_filter = ttk.Checkbutton(
self.track_filter,
text="Enable track filter",
variable=self.variable_enable_track_filter,
command=get_change_enable_track_filter_func(
self.track_filter,
self.frames_track_filter,
self.variable_enable_track_filter
)
)
self.checkbutton_track_filter.grid(row=1, column=1)
self.track_highlight = LabelFrame(self.root, text="Highlight Track")
self.track_highlight.grid(row=3, column=1, sticky="NEWS")
self.variable_enable_track_highlight = BooleanVar(value=False)
self.frames_track_highlight = []
self.checkbutton_track_highlight = ttk.Checkbutton(
self.track_highlight,
text="Enable track highlight",
variable=self.variable_enable_track_highlight,
command=get_change_enable_track_filter_func(
self.track_highlight,
self.frames_track_highlight,
self.variable_enable_track_highlight
)
)
self.checkbutton_track_highlight.grid(row=1, column=1)
self.track_random_new = LabelFrame(self.root, text="Overwrite random cup new")
self.track_random_new.grid(row=4, column=1, sticky="NEWS")
self.variable_enable_track_random_new = BooleanVar(value=False)
self.frames_track_random_new = []
self.checkbutton_track_random_new = ttk.Checkbutton(
self.track_random_new,
text="Enable overwriting random \"new\" track",
variable=self.variable_enable_track_random_new,
command=get_change_enable_track_filter_func(
self.track_random_new,
self.frames_track_random_new,
self.variable_enable_track_random_new
)
)
self.checkbutton_track_random_new.grid(row=1, column=1)
Button(
self.root,
text="Save configuration",
relief=RIDGE,
command=self.save_configuration
).grid(row=100, column=1, sticky="E")
def save_configuration(self):
self.common.ct_config.sort_track_attr = self.combobox_track_sort.get()
self.common.ct_config.filter_track_selection = self.get_filter(
self.variable_enable_track_filter,
self.frames_track_filter
)
self.common.ct_config.filter_track_highlight = self.get_filter(
self.variable_enable_track_highlight,
self.frames_track_highlight
)
self.common.ct_config.filter_track_random_new = self.get_filter(
self.variable_enable_track_random_new,
self.frames_track_random_new
)
def get_filter(self, condition_enabled: BooleanVar, frames_filter: list):
s = lambda x: str(x).strip()
filter_condition = lambda track: True
if not condition_enabled.get(): return filter_condition
next_condition_link_func = lambda a, b: lambda track: a(track) and b(track)
for frame_filter in frames_filter:
track_property = frame_filter["track_property"].get()
value_equal = frame_filter["value_equal"].get()
value_in = frame_filter["value_in"].get()
value_contains = frame_filter["value_contains"].get()
value_between_start = frame_filter["value_between_start"].get()
value_between_end = frame_filter["value_between_end"].get()
def _is_between_func_wrapper(property):
def _is_between_func(track):
from_ = s(value_between_start)
to = s(value_between_end)
prop = s(getattr(track, property, None))
if from_.isnumeric() and prop.isnumeric() and to.isnumeric(): return int(from_) <= int(prop) <= int(to)
else: return from_ <= prop <= to
return _is_between_func
track_conditions_filter = {
self.text_is_equal_to: lambda property: lambda track:
s(getattr(track, property, None)) == s(value_equal),
self.text_is_in: lambda property: lambda track:
s(getattr(track, property, None)) in [s(v) for v in value_in.split(",")],
self.text_is_between:
_is_between_func_wrapper,
self.text_contains: lambda property: lambda track:
s(value_contains) in s(getattr(track, property, None))
}
track_condition_type = frame_filter["condition_type"].get()
track_condition_filter = track_conditions_filter[track_condition_type]
filter_condition = next_condition_link_func(
filter_condition,
track_condition_filter(track_property)
)
next_condition_link = frame_filter["next_condition_link"].get()
next_condition_link_func = self.condition_links[next_condition_link]
return filter_condition

View file

@ -35,10 +35,6 @@ class Track:
"""
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(s)
:param special: track special slot
:param music: track music slot
@ -47,8 +43,6 @@ class Track:
: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 track_wu8_dir: where is stored the track wu8
:param track_szs_dir: where is stored the track szs
:param track_version: version of the track
:param tags: a list of tags that correspond to the track
@ -66,7 +60,6 @@ class Track:
self.warning = warning # Track bug level (1 = minor, 2 = major)
self.version = version
self.tags = tags if tags else []
self.refresh_filename()
self._is_in_group = is_in_group
@ -82,30 +75,34 @@ class Track:
check if track wu8 sha1 is correct
:return: 0 if yes, -1 if no
"""
return check_file_sha1(self._wu8_file, self.sha1)
return check_file_sha1(self.get_wu8_file(), self.sha1)
def check_szs_sha1(self) -> int:
"""
check if track szs sha1 is correct
:return: 0 if yes, -1 if no
"""
return check_file_sha1(self._szs_file, self.sha1)
return check_file_sha1(self.get_szs_file(), self.sha1)
def get_wu8_file(self): return f"{self._wu8_dir}/{self.sha1}.wu8"
def get_szs_file(self): return f"{self._szs_dir}/{self.sha1}.szs"
def convert_wu8_to_szs(self) -> None:
"""
convert track to szs
"""
file_wu8 = f"{self._wu8_dir}/{self.sha1}.wu8"
file_szs = f"{self._szs_dir}/{self.sha1}.szs"
if os.path.exists(file_szs) and os.path.getsize(file_szs) < 1000:
os.remove(file_szs) # File under this size are corrupted
szs_file = self.get_szs_file()
wu8_file = self.get_wu8_file()
if os.path.exists(szs_file) and os.path.getsize(szs_file) < 1000:
os.remove(szs_file) # File under this size are corrupted
if not self.check_szs_sha1(): # if sha1 of track's szs is incorrect or track's szs does not exist
if os.path.exists(file_wu8):
if os.path.exists(wu8_file):
szs.normalize(
src_file=file_wu8,
dest_file=file_szs
src_file=wu8_file,
dest_file=szs_file
)
else:
raise MissingTrackWU8()
@ -116,14 +113,17 @@ class Track:
"""
return self.author if type(self.author) == str else ", ".join(self.author)
def get_ctfile(self, race=False, *args, **kwargs) -> str:
def get_ctfile(self, race=False, filter_random_new=None, *args, **kwargs) -> str:
"""
get ctfile text to create CTFILE.txt and RCTFILE.txt
:param filter_random_new: function to decide if the track should be used by the "random new" option
:param race: is it a text used for Race_*.szs ?
:return: ctfile definition for the track
"""
track_type = "T"
track_flag = 0x00 if self.tag_retro in self.tags else 0x01
if filter_random_new: track_flag = 0x01 if filter_random_new(self) else 0x00
if self._is_in_group:
track_type = "H"
track_flag |= 0x04
@ -152,13 +152,15 @@ class Track:
if tag in tag_list: return tag
return ""
def get_track_formatted_name(self, highlight_version: str = None, *args, **kwargs) -> str:
def get_track_formatted_name(self, filter_highlight=None, *args, **kwargs) -> str:
"""
get the track name with score, color, ...
:param ct_config: ct_config for tags configuration
:param highlight_version: if a specific version need to be highlighted.
:param filter_highlight: filter function to decide if the track should be filtered.
:return: the name of the track with colored prefix, suffix
"""
if not filter_highlight: filter_highlight = lambda track: False
hl_prefix = "" # highlight
hl_suffix = ""
prefix = self.select_tag(self.prefix_list) # tag prefix
@ -174,8 +176,7 @@ class Track:
if 0 < self.warning <= 3:
star_prefix = warning_color[self.warning]
if self.since_version == highlight_version:
hl_prefix, hl_suffix = "\\\\c{blue1}", "\\\\c{off}"
if filter_highlight(self): hl_prefix, hl_suffix = "\\\\c{blue1}", "\\\\c{off}"
if prefix: prefix = "\\\\c{" + self.tags_color[prefix] + "}" + prefix + "\\\\c{off} "
if suffix: suffix = " (\\\\c{" + self.tags_color[suffix] + "}" + suffix + "\\\\c{off})"
@ -190,10 +191,6 @@ class Track:
"""
return f"{self.select_tag(ct_config.prefix_list)}{self.name}{self.select_tag(ct_config.suffix_list)}"
def refresh_filename(self):
self._wu8_file = f"{self._wu8_dir}/{self.sha1}.wu8"
self._szs_file = f"{self._szs_dir}/{self.sha1}.szs"
def load_from_json(self, track_json: dict):
"""
load the track from a dictionary
@ -202,8 +199,6 @@ class Track:
for key, value in track_json.items(): # load all value in the json as class attribute
setattr(self, key, value)
self.refresh_filename()
return self
def create_from_track_file(self, track_file: str) -> None: