mirror of
https://github.com/Faraphel/Atlas-Install.git
synced 2025-07-05 20:28:27 +02:00
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:
parent
753be7df0d
commit
541a1b0689
7 changed files with 227 additions and 26 deletions
3
Pack/Test/mod_config.json
Normal file
3
Pack/Test/mod_config.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"name": "test"
|
||||
}
|
|
@ -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
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
|
Loading…
Reference in a new issue