mirror of
https://github.com/Faraphel/Atlas-Install.git
synced 2025-07-06 12:48:22 +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
|
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:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue