selecting a mod now work, extraction now have a progress bar, clicking on the install button will start the installation

This commit is contained in:
Faraphel 2022-06-11 18:45:11 +02:00
parent 753be7df0d
commit 541a1b0689
7 changed files with 227 additions and 26 deletions

View file

@ -0,0 +1,3 @@
{
"name": "test"
}

View file

@ -0,0 +1,17 @@
import traceback
from tkinter import messagebox
from typing import Callable
from source.translation import translate as _
def better_gui_error(func: Callable) -> Callable:
"""
Decorator to handle GUI errors.
"""
def wrapper(*args, **kwargs):
try: return func(*args, **kwargs)
except: messagebox.showerror(_("ERROR"), traceback.format_exc())
return wrapper

View file

@ -9,7 +9,9 @@ from tkinter import messagebox
import webbrowser import webbrowser
from typing import Generator from typing import Generator
from source.gui import better_gui_error
from source.mkw.Game import Game from source.mkw.Game import Game
from source.mkw.ModConfig import ModConfig
from source.translation import translate as _ from source.translation import translate as _
from source import event from source import event
from source import * from source import *
@ -94,7 +96,32 @@ class Window(tkinter.Tk):
""" """
# get the generator data yield by the generator function # get the generator data yield by the generator function
for step_data in func_gen: for step_data in func_gen:
if "desc" in step_data: self.progress_bar.set_description(step_data["desc"]) if "description" in step_data: self.progress_bar.set_description(step_data["description"])
if "maximum" in step_data: self.progress_bar.set_maximum(step_data["maximum"])
if "step" in step_data: self.progress_bar.step(step_data["step"])
if "value" in step_data: self.progress_bar.set_value(step_data["value"])
if "determinate" in step_data: self.progress_bar.set_determinate(step_data["determinate"])
def get_mod_config(self) -> ModConfig:
"""
Get the mod configuration
:return: Get the mod configuration
"""
return self.select_pack.mod_config
def get_source_path(self) -> Path:
"""
Get the path of the source game
:return: path of the source game
"""
return self.source_game.get_path()
def get_destination_path(self) -> Path:
"""
Get the path of the destination game
:return: path of the destination game
"""
return self.destination_game.get_path()
# Menu bar # Menu bar
@ -215,7 +242,6 @@ class SourceGame(ttk.LabelFrame):
if not path.exists(): raise SourceGameError(path) if not path.exists(): raise SourceGameError(path)
return path return path
def set_state(self, state: InstallerState) -> None: def set_state(self, state: InstallerState) -> None:
""" """
Set the progress bar state when the installer change state Set the progress bar state when the installer change state
@ -286,15 +312,31 @@ class ButtonInstall(ttk.Button):
super().__init__(master, text="Install", command=self.install) super().__init__(master, text="Install", command=self.install)
@threaded @threaded
@better_gui_error
def install(self): def install(self):
try: try:
self.master.set_state(InstallerState.INSTALLING) self.master.set_state(InstallerState.INSTALLING)
# check if the user entered a source path
source_path = self.master.get_source_path()
if str(source_path) == ".":
messagebox.showerror(_("ERROR"), _("ERROR_INVALID_SOURCE_GAME"))
return
# check if the user entered a destination path
destination_path = self.master.get_destination_path()
if str(destination_path) == ".":
messagebox.showerror(_("ERROR"), _("ERROR_INVALID_DESTINATION_GAME"))
return
# get space remaining on the C: drive # get space remaining on the C: drive
if shutil.disk_usage(".").free < minimum_space_available: if shutil.disk_usage(".").free < minimum_space_available:
if not messagebox.askokcancel(_("WARNING"), _("WARNING_NOT_ENOUGH_SPACE_CONTINUE")): return if not messagebox.askokcancel(_("WARNING"), _("WARNING_LOW_SPACE_CONTINUE")):
return
game = Game(self.master.source_game.get_path()) game = Game(source_path)
self.master.progress_function(game.install_mod()) mod_config = self.master.get_mod_config()
self.master.progress_function(game.install_mod(destination_path, mod_config))
finally: finally:
self.master.set_state(InstallerState.IDLE) self.master.set_state(InstallerState.IDLE)
@ -321,7 +363,7 @@ class ProgressBar(ttk.LabelFrame):
self.progress_bar = ttk.Progressbar(self, orient="horizontal") self.progress_bar = ttk.Progressbar(self, orient="horizontal")
self.progress_bar.grid(row=1, column=1, sticky="nsew") self.progress_bar.grid(row=1, column=1, sticky="nsew")
self.description = ttk.Label(self, text="no process running", anchor="center", font=("TkDefaultFont", 10)) self.description = ttk.Label(self, text="", anchor="center", font=("TkDefaultFont", 10))
self.description.grid(row=2, column=1, sticky="nsew") self.description.grid(row=2, column=1, sticky="nsew")
def set_state(self, state: InstallerState) -> None: def set_state(self, state: InstallerState) -> None:
@ -342,14 +384,95 @@ class ProgressBar(ttk.LabelFrame):
""" """
self.description.config(text=desc) self.description.config(text=desc)
def set_maximum(self, maximum: int) -> None:
"""
Set the progress bar maximum value
:param maximum: the maximum value
:return:
"""
self.progress_bar.configure(maximum=maximum)
def set_value(self, value: int) -> None:
"""
Set the progress bar value
:param value: the value
:return:
"""
self.progress_bar.configure(value=value)
def step(self, value: int = 1) -> None:
"""
Set the progress bar by the value
:param value: the step
:return:
"""
self.progress_bar.step(value)
def set_determinate(self, value: bool) -> None:
"""
Set the progress bar determinate value
:param value: the value
:return:
"""
self.progress_bar.configure(mode="determinate" if value else "indeterminate")
# Combobox to select the pack # Combobox to select the pack
class SelectPack(ttk.Combobox): class SelectPack(ttk.Combobox):
def __init__(self, master: tkinter.Tk): def __init__(self, master: tkinter.Tk):
super().__init__(master) super().__init__(master)
self.mod_config: ModConfig | None = None
self.packs: list[Path] = []
self.refresh_packs()
self.select(index=0)
self.bind("<<ComboboxSelected>>", lambda _: self.select())
def refresh_packs(self) -> None:
"""
Refresh the list of packs
:return:
"""
self.packs = []
for pack in Path("./Pack/").iterdir(): for pack in Path("./Pack/").iterdir():
self.insert(tkinter.END, pack.name) if self.is_valid_pack(pack):
self.packs.append(pack)
self["values"] = [pack.name for pack in self.packs]
def select(self, index: int = None) -> None:
"""
When the selection is changed
:index: the index of the selection. If none, use the selected index
:return:
"""
index = index if index is not None else self.current()
pack = self.packs[index]
self.set_path(pack)
self.set(pack.name)
@better_gui_error
def set_path(self, pack: Path) -> None:
"""
Set the pack to install
:param pack: the pack
:return:
"""
self.mod_config = ModConfig.from_file(pack / "mod_config.json")
@classmethod
def is_valid_pack(cls, path: Path) -> bool:
"""
Check if the path is a valid pack
:param path: the path
:return: True if the path is a valid pack
"""
return all([
(path / "mod_config.json").exists(),
])
def set_state(self, state: InstallerState) -> None: def set_state(self, state: InstallerState) -> None:
""" """

View file

@ -2,6 +2,7 @@ import time
from pathlib import Path from pathlib import Path
from typing import Generator from typing import Generator
from source.mkw.ModConfig import ModConfig
from source.wt.wit import WITPath, Region from source.wt.wit import WITPath, Region
@ -23,19 +24,29 @@ class Game:
""" """
return not any(self.wit_path[f"./files/rel/lecode-{region.value}.bin"].exists() for region in Region) return not any(self.wit_path[f"./files/rel/lecode-{region.value}.bin"].exists() for region in Region)
def extract(self, dest: Path | str) -> Path: def extract(self, dest: Path | str) -> Generator[str, None, Path]:
""" """
Extract the game to the destination directory. If the game is a FST, just copy to the destination Extract the game to the destination directory. If the game is a FST, just copy to the destination
:param dest: destination directory :param dest: destination directory
""" """
return self.wit_path.extract_all(dest) gen = self.wit_path.progress_extract_all(dest)
for gen_data in gen:
yield {
"description": f'EXTRACTING - {gen_data["percentage"]}% - (estimated time remaining: '
f'{gen_data["estimation"] if gen_data["estimation"] is not None else "-:--"})',
def install_mod(self) -> Generator[str, None, None]: "maximum": 100,
"value": gen_data["percentage"],
"determinate": True
}
try: next(gen)
except StopIteration as e:
return e.value
def install_mod(self, dest: Path, mod_config: ModConfig) -> Generator[str, None, None]:
""" """
Patch the game with the mod Patch the game with the mod
:dest: destination directory
:mod_config: mod configuration
""" """
i = 0 yield from self.extract(dest / f"{mod_config.nickname} {mod_config.version}")
while True:
time.sleep(1)
yield {"desc": f"step {i}"}
i += 1

View file

@ -4,10 +4,10 @@ import os
class WTError(Exception): class WTError(Exception):
def __init__(self, tool_path: Path | str, return_code: int): def __init__(self, tools_path: Path | str, return_code: int):
try: try:
error = subprocess.run( error = subprocess.run(
[tool_path, "ERROR", str(return_code)], [tools_path, "ERROR", str(return_code)],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
check=True, check=True,
creationflags=subprocess.CREATE_NO_WINDOW, creationflags=subprocess.CREATE_NO_WINDOW,
@ -15,7 +15,7 @@ class WTError(Exception):
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
error = "- Can't get the error message -" error = "- Can't get the error message -"
super().__init__(f"{tool_path} raised {return_code} :\n{error}\n") super().__init__(f"{tools_path} raised {return_code} :\n{error}\n")
class MissingWTError(Exception): class MissingWTError(Exception):
@ -34,10 +34,10 @@ try: tools_wit_dir = next(tools_dir.glob("./wit*/")) / system
except StopIteration as e: raise MissingWTError("wit") from e except StopIteration as e: raise MissingWTError("wit") from e
def better_error(tool_path: Path | str): def better_wt_error(tools_path: Path | str):
""" """
Raise a better error when the subprocess return with a non 0 value. Raise a better error when the subprocess return with a non 0 value.
:param tool_path: path of the used tools :param tools_path: path of the used tools
:return: wrapper :return: wrapper
""" """
@ -46,7 +46,7 @@ def better_error(tool_path: Path | str):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise WTError(tool_path, e.returncode) from e raise WTError(tools_path, e.returncode) from e
return wrapper return wrapper
@ -90,3 +90,18 @@ def _run_dict(tools_path: Path | str, *args) -> dict:
d[key.strip()] = value d[key.strip()] = value
return d return d
def _run_popen(tools_path: Path | str, *args) -> subprocess.Popen:
"""
Run a command and return the process
:param args: command arguments
:return: the output of the command
"""
return subprocess.Popen(
[tools_path, *args],
stdout=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW,
bufsize=1,
universal_newlines=True,
)

View file

@ -17,7 +17,7 @@ class SZSPath:
def __eq__(self, other: "SZSPath") -> bool: def __eq__(self, other: "SZSPath") -> bool:
return self.path == other.path return self.path == other.path
@better_error(tools_path) @better_wt_error(tools_path)
def _run(self, *args) -> bytes: def _run(self, *args) -> bytes:
""" """
Return a command with wszst and return the output Return a command with wszst and return the output
@ -26,7 +26,7 @@ class SZSPath:
""" """
return _run(tools_path, *args) return _run(tools_path, *args)
@better_error(tools_path) @better_wt_error(tools_path)
def _run_dict(self, *args) -> dict: def _run_dict(self, *args) -> dict:
""" """
Return a dictionary of a command that return value associated to a key with a equal sign Return a dictionary of a command that return value associated to a key with a equal sign

View file

@ -1,8 +1,10 @@
import enum import enum
import re
import shutil import shutil
from typing import Generator
from source.wt import * from source.wt import *
from source.wt import _run, _run_dict from source.wt import _run, _run_dict, _run_popen
tools_path = tools_wit_dir / ("wit.exe" if system == "win64" else "wit") tools_path = tools_wit_dir / ("wit.exe" if system == "win64" else "wit")
@ -50,7 +52,7 @@ class WITPath:
def __eq__(self, other: "WITPath") -> bool: def __eq__(self, other: "WITPath") -> bool:
return self.path == other.path return self.path == other.path
@better_error(tools_path) @better_wt_error(tools_path)
def _run(self, *args) -> bytes: def _run(self, *args) -> bytes:
""" """
Return a command with wit and return the output Return a command with wit and return the output
@ -59,7 +61,16 @@ class WITPath:
""" """
return _run(tools_path, *args) return _run(tools_path, *args)
@better_error(tools_path) @classmethod
def _run_popen(cls, *args) -> subprocess.Popen:
"""
Return a command with wit and return the output
:param args: command arguments
:return: the output of the command
"""
return _run_popen(tools_path, *args)
@better_wt_error(tools_path)
def _run_dict(self, *args) -> dict: def _run_dict(self, *args) -> dict:
""" """
Return a dictionary of a command that return value associated to a key with a equal sign Return a dictionary of a command that return value associated to a key with a equal sign
@ -129,6 +140,27 @@ class WITPath:
""" """
return self["./"].extract(dest, flat=False) return self["./"].extract(dest, flat=False)
def progress_extract_all(self, dest: Path | str) -> Generator[dict, None, Path]:
"""
Extract all the subfiles to the destination directory, yelling the percentage and the estimated time remaining
:param dest: destination directory
:return: the extracted file path
"""
process = self._run_popen("EXTRACT", self.path, "-d", dest, "--progress")
while process.poll() is None:
m = re.match(r'\s*(?P<percentage>\d*)%(?:.*?ETA (?P<estimation>\d*:\d*))?\s*', process.stdout.readline())
if m:
yield {
"percentage": int(m.group("percentage")),
"estimation": m.group("estimation")
}
if process.returncode != 0:
raise WTError(tools_path, process.returncode)
return dest
@property @property
def extension(self) -> Extension: def extension(self) -> Extension:
""" """