From ef24ad43cab002d1ebdb6f38c06ae8795f6f3388 Mon Sep 17 00:00:00 2001 From: tretrauit Date: Thu, 17 Feb 2022 22:02:08 +0700 Subject: [PATCH] Added voiceover language info & archive type and install_game function Available through Installer.get_voiceover_archive_type and Installer.get_voiceover_archive_language Also some other optimizations including not extracting unneeded files from diff archive, deprecate _read_version_from_config function, and added install_game, uninstall_game, voiceover_lang_translate, get_installed_voiceovers --- tests/launcher_api_cn_test.py | 29 ++++-- worthless/gui.py | 11 ++- worthless/installer.py | 173 ++++++++++++++++++++++++++-------- worthless/patcher.py | 4 +- 4 files changed, 160 insertions(+), 57 deletions(-) diff --git a/tests/launcher_api_cn_test.py b/tests/launcher_api_cn_test.py index 315fdbf..e54ad51 100644 --- a/tests/launcher_api_cn_test.py +++ b/tests/launcher_api_cn_test.py @@ -1,38 +1,47 @@ import unittest import asyncio import worthless -from worthless.classes import launcher -client = worthless.Launcher(overseas=False) +from worthless.classes import launcher, installer +game_launcher = worthless.Launcher(overseas=False) +game_installer = worthless.Installer(overseas=False) -class LauncherCNTest(unittest.TestCase): +class LauncherOverseasTest(unittest.TestCase): def test_get_version_info(self): - version_info = asyncio.run(client.get_version_info()) - print("get_version_info test.") - print("get_version_info: ", version_info) - self.assertIsInstance(version_info, dict) + version_info = asyncio.run(game_launcher.get_resource_info()) + print("get_resource_info test.") + print("get_resource_info: ", version_info) + print("raw: ", version_info.raw) + self.assertIsInstance(version_info, installer.Resource) def test_get_launcher_info(self): - launcher_info = asyncio.run(client.get_launcher_info()) + launcher_info = asyncio.run(game_launcher.get_launcher_info()) print("get_launcher_info test.") print("get_launcher_info: ", launcher_info) print("raw: ", launcher_info.raw) self.assertIsInstance(launcher_info, launcher.Info) def test_get_launcher_full_info(self): - launcher_info = asyncio.run(client.get_launcher_full_info()) + launcher_info = asyncio.run(game_launcher.get_launcher_full_info()) print("get_launcher_full_info test.") print("get_launcher_full_info: ", launcher_info) print("raw: ", launcher_info.raw) self.assertIsInstance(launcher_info, launcher.Info) def test_get_launcher_background_url(self): - bg_url = asyncio.run(client.get_launcher_background_url()) + bg_url = asyncio.run(game_launcher.get_launcher_background_url()) print("get_launcher_background_url test.") print("get_launcher_background_url: ", bg_url) self.assertIsInstance(bg_url, str) self.assertTrue(bg_url) + def test_get_installer_diff(self): + game_diff = asyncio.run(game_installer.get_game_diff_archive("2.4.0")) + print("get_game_diff_archive test.") + print("get_game_diff_archive: ", game_diff) + print("raw: ", game_diff.raw) + self.assertIsInstance(game_diff, installer.Diff) + if __name__ == '__main__': unittest.main() diff --git a/worthless/gui.py b/worthless/gui.py index 97107b9..c887eef 100755 --- a/worthless/gui.py +++ b/worthless/gui.py @@ -32,14 +32,17 @@ class UI: def _update_from_archive(self, filepath): print("Reverting patches if patched...") self._patcher.revert_patch(True) - print("Updating game from archive...") + print("Updating game from archive (this may takes some time)...") self._installer.update_game(filepath) def _apply_voiceover_from_archive(self, filepath): - print("Applying voiceover from archive...") + print("Applying voiceover from archive (this may takes some time)...") self._installer.apply_voiceover(filepath) def install_voiceover_from_file(self, filepath): + print("Archive voiceover language: {} ({})".format( + self._installer.get_voiceover_archive_language(filepath), + "Full archive" if self._installer.get_voiceover_archive_type(filepath) else "Update archive")) if not self._ask("Do you want to apply this voiceover pack? ({})".format(filepath)): print("Aborting apply process.") return @@ -55,8 +58,8 @@ class UI: gamever = self._installer.get_game_version() if gamever: print("Current game installation detected. ({})".format(self._installer.get_game_version())) - print("Archive game version: " + self._installer.get_archive_version(filepath)) - if not self._ask("Do you want to update the game? (from {})".format(filepath)): + print("Archive game version: " + self._installer.get_game_archive_version(filepath)) + if not self._ask("Do you want to update the game? ({})".format(filepath)): print("Aborting update process.") return self._update_from_archive(filepath) diff --git a/worthless/installer.py b/worthless/installer.py index 09dd071..8e3f448 100644 --- a/worthless/installer.py +++ b/worthless/installer.py @@ -1,6 +1,10 @@ import re +import shutil + import appdirs import zipfile +import warnings +import json from pathlib import Path from configparser import ConfigParser @@ -8,26 +12,36 @@ from worthless import constants from worthless.launcher import Launcher -def read_version_from_game_file(globalgamemanagers: Path | bytes): - if isinstance(globalgamemanagers, Path): - with globalgamemanagers.open("rb") as f: - data = f.read().decode("ascii", errors="ignore") - else: - data = globalgamemanagers.decode("ascii", errors="ignore") - result = re.search(r"([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+", data) - if not result: - raise ValueError("Could not find version in game file") - return result.group(1) - - class Installer: def _read_version_from_config(self): + warnings.warn("This function is not reliable as upgrading game version from worthless\ + doesn't write the config.", DeprecationWarning) if not self._config_file.exists(): raise FileNotFoundError(f"Config file {self._config_file} not found") cfg = ConfigParser() cfg.read(str(self._config_file)) return cfg.get("General", "game_version") + @staticmethod + def read_version_from_game_file(globalgamemanagers: Path | bytes): + """ + Reads the version from the globalgamemanagers file. (Data/globalgamemanagers) + + Uses `An Anime Game Launcher` method to read the version: + https://gitlab.com/KRypt0n_/an-anime-game-launcher/-/blob/main/src/ts/Game.ts#L26 + + :return: Game version (ex 1.0.0) + """ + if isinstance(globalgamemanagers, Path): + with globalgamemanagers.open("rb") as f: + data = f.read().decode("ascii", errors="ignore") + else: + data = globalgamemanagers.decode("ascii", errors="ignore") + result = re.search(r"([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+", data) + if not result: + raise ValueError("Could not find version in game file") + return result.group(1) + def get_game_data_name(self): if self._overseas: return "GenshinImpact_Data/" @@ -37,15 +51,23 @@ class Installer: def get_game_data_path(self): return self._gamedir.joinpath(self.get_game_data_name()) - # https://gitlab.com/KRypt0n_/an-anime-game-launcher/-/blob/main/src/ts/Game.ts#L26 def get_game_version(self): - if self._config_file.exists(): - return self._read_version_from_config() - else: - globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers") - if not globalgamemanagers.exists(): - return - return read_version_from_game_file(globalgamemanagers) + globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers") + if not globalgamemanagers.exists(): + return + return self.read_version_from_game_file(globalgamemanagers) + + def get_installed_voiceovers(self): + """ + Returns a list of installed voiceovers. + + :return: List of installed voiceovers + """ + voiceovers = [] + for file in self.get_game_data_path().joinpath("StreamingAssets/Audio/GeneratedSoundBanks/Windows").iterdir(): + if file.is_dir(): + voiceovers.append(file.name) + return voiceovers def __init__(self, gamedir: str | Path = Path.cwd(), overseas: bool = True, data_dir: str | Path = None): if isinstance(gamedir, str): @@ -65,17 +87,60 @@ class Installer: self._launcher = Launcher(self._gamedir, overseas=self._overseas) self._version = self.get_game_version() - def get_archive_version(self, game_archive: str | Path): + def get_game_archive_version(self, game_archive: str | Path): if not game_archive.exists(): raise FileNotFoundError(f"Game archive {game_archive} not found") archive = zipfile.ZipFile(game_archive, 'r') - return read_version_from_game_file(archive.read(self.get_game_data_name() + "globalgamemanagers")) + return self.read_version_from_game_file(archive.read(self.get_game_data_name() + "globalgamemanagers")) + + @staticmethod + def voiceover_lang_translate(lang: str): + """ + Translates the voiceover language to the language code used by the game. + + :param lang: Language to translate + :return: Language code + """ + match lang: + case "English(US)": + return "en-us" + case "Japanese": + return "ja-jp" + case "Chinese": + return "zh-cn" + case "Korean": + return "ko-kr" + + @staticmethod + def get_voiceover_archive_language(voiceover_archive: str | Path): + if isinstance(voiceover_archive, str): + voiceover_archive = Path(voiceover_archive).resolve() + if not voiceover_archive.exists(): + raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found") + archive = zipfile.ZipFile(voiceover_archive, 'r') + archive_path = zipfile.Path(archive) + for file in archive_path.iterdir(): + if file.name.endswith("_pkg_version"): + return file.name.split("_")[1] + + def get_voiceover_archive_type(self, voiceover_archive: str | Path): + vo_lang = self.get_voiceover_archive_language(voiceover_archive) + archive = zipfile.ZipFile(voiceover_archive, 'r') + archive_path = zipfile.Path(archive) + files = archive.read("Audio_{}_pkg_version".format(vo_lang)).decode().split("\n") + for file in files: + if file.strip() and not archive_path.joinpath(json.loads(file)["remoteName"]).exists(): + return False + return True def apply_voiceover(self, voiceover_archive: str | Path): + # Since Voiceover packages are unclear about diff package or full package + # we will try to extract the voiceover package and apply it to the game + # making this function universal for both cases if not self.get_game_data_path().exists(): raise FileNotFoundError(f"Game not found in {self._gamedir}") if isinstance(voiceover_archive, str): - game_archive = Path(voiceover_archive).resolve() + voiceover_archive = Path(voiceover_archive).resolve() if not voiceover_archive.exists(): raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found") archive = zipfile.ZipFile(voiceover_archive, 'r') @@ -90,6 +155,15 @@ class Installer: if not game_archive.exists(): raise FileNotFoundError(f"Update archive {game_archive} not found") archive = zipfile.ZipFile(game_archive, 'r') + 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) + for file in ["deletefiles.txt", "hdifffiles.txt"]: + try: + files.remove(file) + except ValueError: + pass + deletefiles = archive.read("deletefiles.txt").decode().split("\n") for file in deletefiles: current_game_file = self._gamedir.joinpath(file) @@ -97,28 +171,45 @@ class Installer: continue if current_game_file.is_file(): current_game_file.unlink(missing_ok=True) - archive.extractall(self._gamedir) - archive.close() - self._gamedir.joinpath("deletefiles.txt").unlink(missing_ok=True) - self._gamedir.joinpath("hdifffiles.txt").unlink(missing_ok=True) - async def install_game(self, force_reinstall: bool = False): + archive.extractall(self._gamedir, members=files) + archive.close() + + async def download_game_update(self): + if self._version is None: + raise ValueError("Game version not found, use install_game to install the game.") + version_info = await self._launcher.get_resource_info() + if version_info is None: + raise RuntimeError("Failed to fetch game resource info.") + if self._version == version_info.game.latest.version: + raise ValueError("Game is already up to date.") + diff_archive = self.get_game_diff_archive() + if diff_archive is None: + raise ValueError("Game diff archive is not available for this version, please reinstall.") + # TODO: Download the diff archive + raise NotImplementedError("Downloading game diff archive is not implemented yet.") + + def uninstall_game(self): + shutil.rmtree(self._gamedir) + + def install_game(self, game_archive: str | Path, force_reinstall: bool = False): """Installs the game to the current directory - If `from_version` is not specified, it will be taken from the game version. - If `to_version` is not specified, it will be taken from the game version. + If `force_reinstall` is True, the game will be uninstalled then reinstalled. """ - raise NotImplementedError("Not implemented yet") - # if not force: - # if self._temp_path.exists(): - # raise FileExistsError(f"Directory {self._temp_path} already exists") - # self._temp_path.mkdir(parents=True, exist_ok=True) - # self._launcher.set_temp_path(self._temp_path) - # await self._launcher.download_game_diff_archive(from_version, to_version) - # await self._launcher.extract_game_diff_archive() - # await self._launcher.install_game_diff_archive() - # self._launcher.set_temp_path(None) - # self._temp_path.rmdir() + if self.get_game_data_path().exists(): + if not force_reinstall: + raise ValueError(f"Game is already installed in {self._gamedir}") + self.uninstall_game() + + self._gamedir.mkdir(parents=True, exist_ok=True) + if isinstance(game_archive, str): + game_archive = Path(game_archive).resolve() + if not game_archive.exists(): + raise FileNotFoundError(f"Install archive {game_archive} not found") + archive = zipfile.ZipFile(game_archive, 'r') + archive.extractall(self._gamedir) + archive.close() async def get_game_diff_archive(self, from_version: str = None): """Gets a diff archive from `from_version` to the latest one diff --git a/worthless/patcher.py b/worthless/patcher.py index b6e006e..79c6419 100644 --- a/worthless/patcher.py +++ b/worthless/patcher.py @@ -127,8 +127,8 @@ class Patcher: ] for file in revert_files: self._revert_file(file, game_exec, ignore_errors) - self._gamedir.joinpath("launcher.bat").unlink(missing_ok=True) - self._gamedir.joinpath("mhyprot2_running.reg").unlink(missing_ok=True) + for file in ["launcher.bat", "mhyprot2_running.reg"]: + self._gamedir.joinpath(file).unlink(missing_ok=True) def get_files(extensions): all_files = []