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
from typing import Generator
from source.gui import better_gui_error
from source.mkw.Game import Game
from source.mkw.ModConfig import ModConfig
from source.translation import translate as _
from source import event
from source import *
@ -94,7 +96,32 @@ class Window(tkinter.Tk):
"""
# get the generator data yield by the generator function
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
@ -215,7 +242,6 @@ class SourceGame(ttk.LabelFrame):
if not path.exists(): raise SourceGameError(path)
return path
def set_state(self, state: InstallerState) -> None:
"""
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)
@threaded
@better_gui_error
def install(self):
try:
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
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())
self.master.progress_function(game.install_mod())
game = Game(source_path)
mod_config = self.master.get_mod_config()
self.master.progress_function(game.install_mod(destination_path, mod_config))
finally:
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.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")
def set_state(self, state: InstallerState) -> None:
@ -342,14 +384,95 @@ class ProgressBar(ttk.LabelFrame):
"""
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
class SelectPack(ttk.Combobox):
def __init__(self, master: tkinter.Tk):
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():
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:
"""

View file

@ -2,6 +2,7 @@ import time
from pathlib import Path
from typing import Generator
from source.mkw.ModConfig import ModConfig
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)
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
: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
:dest: destination directory
:mod_config: mod configuration
"""
i = 0
while True:
time.sleep(1)
yield {"desc": f"step {i}"}
i += 1
yield from self.extract(dest / f"{mod_config.nickname} {mod_config.version}")

View file

@ -4,10 +4,10 @@ import os
class WTError(Exception):
def __init__(self, tool_path: Path | str, return_code: int):
def __init__(self, tools_path: Path | str, return_code: int):
try:
error = subprocess.run(
[tool_path, "ERROR", str(return_code)],
[tools_path, "ERROR", str(return_code)],
stdout=subprocess.PIPE,
check=True,
creationflags=subprocess.CREATE_NO_WINDOW,
@ -15,7 +15,7 @@ class WTError(Exception):
except subprocess.CalledProcessError as e:
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):
@ -34,10 +34,10 @@ try: tools_wit_dir = next(tools_dir.glob("./wit*/")) / system
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.
:param tool_path: path of the used tools
:param tools_path: path of the used tools
:return: wrapper
"""
@ -46,7 +46,7 @@ def better_error(tool_path: Path | str):
try:
return func(*args, **kwargs)
except subprocess.CalledProcessError as e:
raise WTError(tool_path, e.returncode) from e
raise WTError(tools_path, e.returncode) from e
return wrapper
@ -90,3 +90,18 @@ def _run_dict(tools_path: Path | str, *args) -> dict:
d[key.strip()] = value
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:
return self.path == other.path
@better_error(tools_path)
@better_wt_error(tools_path)
def _run(self, *args) -> bytes:
"""
Return a command with wszst and return the output
@ -26,7 +26,7 @@ class SZSPath:
"""
return _run(tools_path, *args)
@better_error(tools_path)
@better_wt_error(tools_path)
def _run_dict(self, *args) -> dict:
"""
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 re
import shutil
from typing import Generator
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")
@ -50,7 +52,7 @@ class WITPath:
def __eq__(self, other: "WITPath") -> bool:
return self.path == other.path
@better_error(tools_path)
@better_wt_error(tools_path)
def _run(self, *args) -> bytes:
"""
Return a command with wit and return the output
@ -59,7 +61,16 @@ class WITPath:
"""
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:
"""
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)
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
def extension(self) -> Extension:
"""