diff --git a/docs/common/functions.md b/docs/common/functions.md index d337612..e66dc99 100644 --- a/docs/common/functions.md +++ b/docs/common/functions.md @@ -1,4 +1,4 @@ -# Common function for all games +# Function for all games Since you should use the specific implementation for the target game instead, these functions may not be correctly documented. diff --git a/vollerei/abc/launcher/game.py b/vollerei/abc/launcher/game.py index 225dd18..663866a 100644 --- a/vollerei/abc/launcher/game.py +++ b/vollerei/abc/launcher/game.py @@ -114,7 +114,7 @@ class GameABC(ABC): """ pass - def get_update(self): + def get_update(self) -> resource.Patch | None: """ Get the game update """ @@ -126,7 +126,9 @@ class GameABC(ABC): """ pass - def get_remote_game(self, pre_download: bool = False) -> resource.Main | resource.PreDownload: + def get_remote_game( + self, pre_download: bool = False + ) -> resource.Main | resource.PreDownload: """ Gets the current game information from remote. diff --git a/vollerei/cli/commands.py b/vollerei/cli/commands.py index 41c63a8..8cc944b 100644 --- a/vollerei/cli/commands.py +++ b/vollerei/cli/commands.py @@ -5,6 +5,7 @@ from cleo.helpers import option, argument from pathlib import PurePath from platform import system from vollerei.abc.launcher.game import GameABC +from vollerei.common.api import resource from vollerei.common.enums import GameChannel, VoicePackLanguage from vollerei.cli import utils from vollerei.exceptions.game import GameError @@ -141,9 +142,7 @@ class VoicepackList(Command): class VoicepackInstall(Command): name = "hsr voicepack install" - description = ( - "Installs the specified installed voicepacks" - ) + description = "Installs the specified installed voicepacks" options = default_options + [ option("pre-download", description="Pre-download the game if available"), ] @@ -196,7 +195,9 @@ class VoicepackInstall(Command): self.line( f"Downloading install package for language: {remote_voicepack.language.name}... " ) - archive_file = State.game.cache.joinpath(PurePath(remote_voicepack.url).name) + archive_file = State.game.cache.joinpath( + PurePath(remote_voicepack.url).name + ) try: download_result = utils.download( remote_voicepack.url, archive_file, file_len=remote_voicepack.size @@ -280,7 +281,7 @@ class VoicepackUpdate(Command): progress = utils.ProgressIndicator(self) progress.start("Checking for updates... ") try: - update_diff = State.game.get_update(pre_download=pre_download) + update_diff: resource.Patch | None = State.game.get_update(pre_download=pre_download) game_info = State.game.get_remote_game(pre_download=pre_download) except Exception as e: progress.finish( @@ -295,23 +296,25 @@ class VoicepackUpdate(Command): f"The current version is: {State.game.get_version_str()}" ) self.line( - f"The latest version is: {game_info.latest.version}" + f"The latest version is: {game_info.major.version}" ) if not self.confirm("Do you want to update the game?"): self.line("Update aborted.") return # Voicepack update - for remote_voicepack in update_diff.voice_packs: + for remote_voicepack in update_diff.audio_pkgs: if remote_voicepack.language not in installed_voicepacks: continue # Voicepack is installed, update it - self.line( - f"Downloading update package for language: {remote_voicepack.language.name}... " + archive_file = State.game.cache.joinpath( + PurePath(remote_voicepack.url).name + ) + self.line( + f"Downloading update package for voicepack language '{remote_voicepack.language.name}'..." ) - archive_file = State.game.cache.joinpath(remote_voicepack.name) try: download_result = utils.download( - remote_voicepack.path, archive_file, file_len=update_diff.size + remote_voicepack.url, archive_file, file_len=remote_voicepack.size ) except Exception as e: self.line_error(f"Couldn't download update: {e}") @@ -334,10 +337,9 @@ class VoicepackUpdate(Command): progress.finish( f"Update applied for language {remote_voicepack.language.name}." ) + State.game.version_override = game_info.major.version set_version_config(self=self) - self.line( - f"The game has been updated to version: {State.game.get_version_str()}" - ) + State.game.version_override = None class PatchTypeCommand(Command): @@ -601,7 +603,7 @@ class InstallCommand(Command): progress.finish("Package applied for the base game.") self.line("Setting version config... ") State.game.version_override = game_info.major.version - set_version_config() + set_version_config(self=self) State.game.version_override = None self.line( f"The game has been installed to version: {State.game.get_version_str()}" @@ -719,7 +721,7 @@ class UpdateCommand(Command): ) self.line("Setting version config... ") State.game.version_override = game_info.major.version - set_version_config() + set_version_config(self=self) State.game.version_override = None self.line( f"The game has been updated to version: {State.game.get_version_str()}" @@ -940,7 +942,8 @@ class ApplyUpdateArchive(Command): ) return progress.finish("Update applied.") - set_version_config() + set_version_config(self=self) + # This is the list for HSR commands, we'll add Genshin commands later classes = [ diff --git a/vollerei/common/enums.py b/vollerei/common/enums.py index 70936fb..c562179 100644 --- a/vollerei/common/enums.py +++ b/vollerei/common/enums.py @@ -49,4 +49,4 @@ class VoicePackLanguage(Enum): elif s == "En": return VoicePackLanguage.English else: - raise ValueError(f"Invalid language string: {s}") \ No newline at end of file + raise ValueError(f"Invalid language string: {s}") diff --git a/vollerei/common/functions.py b/vollerei/common/functions.py index 74518d9..2703a45 100644 --- a/vollerei/common/functions.py +++ b/vollerei/common/functions.py @@ -2,6 +2,7 @@ import concurrent.futures import json import hashlib import py7zr +import zipfile from io import IOBase from os import PathLike from pathlib import Path @@ -39,9 +40,30 @@ def apply_update_archive( # Install HDiffPatch _hdiff.hpatchz() + # Open archive - # archive = zipfile.ZipFile(archive_file, "r") - archive = py7zr.SevenZipFile(archive_file, "r") + def reset_if_py7zr(archive): + if isinstance(archive, py7zr.SevenZipFile): + archive.reset() + + def extract_files(archive: py7zr.SevenZipFile | zipfile.ZipFile, files, path: PathLike): + if isinstance(archive, py7zr.SevenZipFile): + # .7z archive + archive.extract(path, files) + else: + # .zip archive + archive.extractall(path, files) + + archive: py7zr.SevenZipFile | zipfile.ZipFile = None + try: + archive = py7zr.SevenZipFile(archive_file, "r") + except py7zr.exceptions.Bad7zFile: + # Try to open it as a zip file + try: + archive = zipfile.ZipFile(archive_file, "r") + except zipfile.BadZipFile: + raise ValueError("Archive is not a valid 7z or zip file.") + # Get files list (we don't want to extract all of them) files = archive.namelist() # Don't extract these files (they're useless and if the game isn't patched then @@ -52,18 +74,23 @@ def apply_update_archive( except ValueError: pass # Think for me a better name for this variable - txtfiles = archive.read(["deletefiles.txt", "hdifffiles.txt"]) - # Reset archive to extract files - archive.reset() + txtfiles = None + if isinstance(archive, py7zr.SevenZipFile): + txtfiles = archive.read(["deletefiles.txt", "hdifffiles.txt"]) + # Reset archive to extract files + archive.reset() try: # miHoYo loves CRLF - deletebytes = txtfiles["deletefiles.txt"].read() + if txtfiles is not None: + deletebytes = txtfiles["deletefiles.txt"].read() + else: + deletebytes = archive.read("deletefiles.txt") if deletebytes is not str: # Typing deletebytes: bytes deletebytes = deletebytes.decode() deletefiles = deletebytes.split("\r\n") - except IOError: + except (IOError, KeyError): pass else: for file_str in deletefiles: @@ -80,7 +107,10 @@ def apply_update_archive( # hdiffpatch implementation # Read hdifffiles.txt to get the files to patch hdifffiles = [] - hdiffbytes = txtfiles["hdifffiles.txt"].read() + if txtfiles is not None: + hdiffbytes = txtfiles["hdifffiles.txt"].read() + else: + hdiffbytes = archive.read("hdifffiles.txt") if hdiffbytes is not str: # Typing hdiffbytes: bytes @@ -132,8 +162,8 @@ def apply_update_archive( patch_jobs.append([patch, [file, patch_file]]) # Extract patch files to temporary dir - archive.extract(game.cache, patch_files) - archive.reset() # For the next extraction + extract_files(archive, patch_files, game.cache) + reset_if_py7zr(archive) # For the next extraction # Create new ThreadPoolExecutor for patching patch_executor = concurrent.futures.ThreadPoolExecutor() for job in patch_jobs: @@ -141,7 +171,7 @@ def apply_update_archive( patch_executor.shutdown(wait=True) # Extract files from archive after we have filtered out the patch files - archive.extract(game.path, files) + extract_files(archive, files, game.path) # Close the archive archive.close() diff --git a/vollerei/zzz/launcher/game.py b/vollerei/zzz/launcher/game.py index a9c6b67..e7e98c3 100644 --- a/vollerei/zzz/launcher/game.py +++ b/vollerei/zzz/launcher/game.py @@ -30,7 +30,7 @@ class Game(GameABC): if not cache_path: cache_path = paths.cache_path cache_path = Path(cache_path) - self.cache: Path = cache_path.joinpath("game/genshin/") + self.cache: Path = cache_path.joinpath("game/zzz/") self.cache.mkdir(parents=True, exist_ok=True) self._version_override: tuple[int, int, int] | None = None self._channel_override: GameChannel | None = None @@ -192,7 +192,7 @@ class Game(GameABC): Gets the current installed game version. Credits to An Anime Team for the code that does the magic: - https://github.com/an-anime-team/anime-game-core/blob/main/src/games/genshin/game.rs#L52 + https://github.com/an-anime-team/anime-game-core/blob/main/src/games/zzz/game.rs#L49 If the above method fails, it'll fallback to read the config.ini file for the version, which is not recommended (as described in @@ -224,25 +224,18 @@ class Game(GameABC): for byte in f.read(10000): match byte: case 0: + if correct and len(version_bytes[0]) > 0 and len(version_bytes[1]) > 0 and len(version_bytes[2]) > 0: + found_version = tuple( + bytes_to_int(i) for i in version_bytes + ) + return found_version version_bytes = [[], [], []] version_ptr = 0 correct = True - case 46: + case b'.': version_ptr += 1 if version_ptr > 2: correct = False - case 95: - if ( - correct - and len(version_bytes[0]) > 0 - and len(version_bytes[1]) > 0 - and len(version_bytes[2]) > 0 - ): - return ( - bytes_to_int(version_bytes[0]), - bytes_to_int(version_bytes[1]), - bytes_to_int(version_bytes[2]), - ) case _: if correct and byte in b"0123456789": version_bytes[version_ptr].append(byte) @@ -276,9 +269,7 @@ class Game(GameABC): raise GameNotInstalledError("Game is not installed.") voicepacks = [] for child in ( - self.data_folder() - .joinpath("StreamingAssets/Audio/Windows/Full/") - .iterdir() + self.data_folder().joinpath("StreamingAssets/Audio/Windows/Full/").iterdir() ): if child.resolve().is_dir(): try: