diff --git a/setup.py b/setup.py index 79d3f15..e879fb1 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ README = (HERE / "README.md").read_text() setup( name='worthless', - version='1.2.9-4', + version='1.3.0', packages=['worthless', 'worthless.classes', 'worthless.classes.launcher', 'worthless.classes.installer'], url='https://git.froggi.es/tretrauit/worthless-launcher', license='MIT License', diff --git a/worthless/__init__.py b/worthless/__init__.py index 583ae8d..87899de 100644 --- a/worthless/__init__.py +++ b/worthless/__init__.py @@ -2,3 +2,4 @@ from worthless import launcher, installer Launcher = launcher.Launcher Installer = installer.Installer + diff --git a/worthless/classes/__init__.py b/worthless/classes/__init__.py index e69de29..8b13789 100644 --- a/worthless/classes/__init__.py +++ b/worthless/classes/__init__.py @@ -0,0 +1 @@ + diff --git a/worthless/gui.py b/worthless/gui.py index 14f1ddb..04dda37 100755 --- a/worthless/gui.py +++ b/worthless/gui.py @@ -59,11 +59,13 @@ class UI: print("Reverting patches if patched...") self._patcher.revert_patch(True) print("Updating game from archive (this may takes some time)...") - self._installer.update_game(filepath) + asyncio.run(self._installer.update_game(filepath)) + self._installer.set_version_config() def _install_from_archive(self, filepath, force_reinstall): print("Installing game from archive (this may takes some time)...") self._installer.install_game(filepath, force_reinstall) + self._installer.set_version_config() def _apply_voiceover_from_archive(self, filepath): print("Applying voiceover from archive (this may takes some time)...") @@ -311,6 +313,9 @@ def main(): if args.install: ui.install_game() + if args.update_all: + raise NotImplementedError() # TODO + if args.update: ui.update_game_voiceover(args.update) diff --git a/worthless/installer.py b/worthless/installer.py index 3b669a8..9bff2b2 100644 --- a/worthless/installer.py +++ b/worthless/installer.py @@ -1,3 +1,4 @@ +import asyncio import re import shutil import platform @@ -11,9 +12,11 @@ from configparser import ConfigParser from aiopath import AsyncPath from worthless import constants from worthless.launcher import Launcher +from worthless.launcherconfig import LauncherConfig -async def _download_file(file_url: str, file_name: str, file_path: Path | str, file_len: int = None, overwrite=False, chunks=8192): +async def _download_file(file_url: str, file_name: str, file_path: Path | str, file_len: int = None, overwrite=False, + chunks=8192): """ Download file name to temporary directory, :param file_url: @@ -45,7 +48,7 @@ async def _download_file(file_url: str, file_name: str, file_path: Path | str, f class HDiffPatch: def __init__(self, git_url=None, data_dir=None): if not git_url: - repo_url = constants.HDIFFPATCH_GIT_URL + git_url = constants.HDIFFPATCH_GIT_URL self._git_url = git_url if not data_dir: self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR) @@ -82,6 +85,8 @@ class HDiffPatch: def _get_hdiffpatch_exec(self, exec_name): if shutil.which(exec_name): return exec_name + if not self.data_path.exists(): + return None if not any(self.data_path.iterdir()): return None platform_arch_path = self.data_path.joinpath(self._get_platform_arch()) @@ -92,6 +97,12 @@ class HDiffPatch: def get_hpatchz_executable(self): return self._get_hdiffpatch_exec("hpatchz") + async def patch_file(self, in_file, out_file, patch_file): + hpatchz = self.get_hpatchz_executable() + if not hpatchz: + raise RuntimeError("hpatchz executable not found") + return await asyncio.create_subprocess_exec(hpatchz, "-f", in_file, patch_file, out_file) + def get_hdiffz_executable(self): return self._get_hdiffpatch_exec("hdiffz") @@ -103,9 +114,9 @@ class HDiffPatch: rsp = await session.get("https://api.github.com/repos/{}/{}/releases/latest".format(owner, repo), params={"Headers": "Accept: application/vnd.github.v3+json"}) rsp.raise_for_status() - for asset in await rsp.json()["assets"]: - if asset["name"].endswith(".zip") and not "linux" in asset["name"] and not "windows" in asset["name"] \ - and not "macos" in asset["name"] and not "android" in asset["name"]: + for asset in (await rsp.json())["assets"]: + if asset["name"].endswith(".zip") and "linux" not in asset["name"] and "windows" not in asset["name"] \ + and "macos" not in asset["name"] and "android" not in asset["name"]: return asset async def get_latest_release_url(self): @@ -201,11 +212,11 @@ class Installer: config_file = self._gamedir.joinpath("config.ini") self._config_file = config_file.resolve() self._download_chunk = 8192 - self._version = None self._overseas = overseas + self._version = self.get_game_version() self._launcher = Launcher(self._gamedir, overseas=self._overseas) self._hdiffpatch = HDiffPatch(data_dir=data_dir) - self._version = self.get_game_version() + self._config = LauncherConfig(self._config_file, self._version) def set_download_chunk(self, chunk: int): self._download_chunk = chunk @@ -280,7 +291,7 @@ class Installer: archive.extractall(self._gamedir) archive.close() - def update_game(self, game_archive: str | Path): + async def update_game(self, game_archive: str | Path): if not self.get_game_data_path().exists(): raise FileNotFoundError(f"Game not found in {self._gamedir}") if isinstance(game_archive, str): @@ -288,6 +299,10 @@ class Installer: if not game_archive.exists(): raise FileNotFoundError(f"Update archive {game_archive} not found") archive = zipfile.ZipFile(game_archive, 'r') + + if not self._hdiffpatch.get_hpatchz_executable(): + await self._hdiffpatch.download_latest_release() + files = archive.namelist() # Don't extract these files (they're useless and if the game isn't patched then it'll # raise 31-4xxx error ingame) @@ -297,6 +312,35 @@ class Installer: except ValueError: pass + # hdiffpatch implementation + hdifffiles = [] + for x in archive.read("hdifffiles.txt").decode().split("\n"): + if x: + hdifffiles.append(json.loads(x)["remoteName"]) + patch_jobs = [] + for file in hdifffiles: + current_game_file = self._gamedir.joinpath(file) + if not current_game_file.exists(): + # Not patching since we don't have the file + continue + + patch_file = str(file) + ".hdiff" + + async def extract_and_patch(old_file, diff_file): + await asyncio.to_thread(archive.extract, diff_file, self.temp_path) + patch_path = self.temp_path.joinpath(diff_file) + old_suffix = old_file.suffix + old_file = old_file.rename(old_file.with_suffix(".bak")) + proc = await self._hdiffpatch.patch_file(old_file, old_file.with_suffix(old_suffix), + patch_path) + await proc.wait() + patch_path.unlink() + old_file.unlink() + + files.remove(patch_file) + patch_jobs.append(extract_and_patch(current_game_file, patch_file)) + await asyncio.gather(*patch_jobs) + deletefiles = archive.read("deletefiles.txt").decode().split("\n") for file in deletefiles: current_game_file = self._gamedir.joinpath(file) @@ -310,6 +354,12 @@ class Installer: # Update game version on local variable. self._version = self.get_game_version() + def set_version_config(self, version: str = None): + if not version: + version = self._version + self._config.set_game_version(version) + self._config.save() + async def download_full_game(self, overwrite: bool = False): if self._version and not overwrite: raise ValueError("Game already exists") diff --git a/worthless/launcherconfig.py b/worthless/launcherconfig.py new file mode 100644 index 0000000..5446bd9 --- /dev/null +++ b/worthless/launcherconfig.py @@ -0,0 +1,49 @@ +from configparser import ConfigParser +from pathlib import Path + + +class LauncherConfig: + """ + Provides config.ini for official launcher compatibility + """ + + @staticmethod + def create_config(game_version, overseas=True): + """ + Creates config.ini + """ + sub_channel = "0" if overseas else "1" + config = ConfigParser() + config.add_section("General") + config.set("General", "channel", "1") + config.set("General", "cps", "mihoyo") + config.set("General", "game_version", game_version) + config.set("General", "sdk_version", "") + config.set("General", "sub_channel", sub_channel) + return config + + def __init__(self, config_path, game_version=None, overseas=True): + if isinstance(config_path, str): + self.config_path = Path(config_path) + if not game_version: + game_version = "0.0.0" + self.config_path = config_path + self.config = ConfigParser() + if self.config_path.exists(): + self.config.read(self.config_path) + else: + self.config = self.create_config(game_version, overseas) + + def set_game_version(self, game_version): + self.config.set("General", "game_version", game_version) + + def set_overseas(self, overseas=True): + sub_channel = "0" if overseas else "1" + self.config.set("General", "sub_channel", sub_channel) + + def save(self): + """ + Saves config.ini + """ + with self.config_path.open("w") as config_file: + self.config.write(config_file)