diff --git a/vollerei/cli/commands.py b/vollerei/cli/commands.py new file mode 100644 index 0000000..960b2d3 --- /dev/null +++ b/vollerei/cli/commands.py @@ -0,0 +1,968 @@ +# THIS FILE CURRENTLY DOESN'T WORK YET. +import copy +import traceback +from cleo.commands.command import Command +from cleo.helpers import option, argument +from pathlib import PurePath +from platform import system +from vollerei.abc.launcher.game import GameABC +from vollerei.common.enums import GameChannel, VoicePackLanguage +from vollerei.cli import utils +from vollerei.exceptions.game import GameError +from vollerei.exceptions.patcher import PatcherError, PatchUpdateError +from vollerei.genshin import Game as GenshinGame +from vollerei.hsr import Game as HSRGame, Patcher as HSRPatcher +from vollerei.hsr.patcher import PatchType as HSRPatchType +from vollerei import paths + +patcher = HSRPatcher() + + +default_options = [ + option("channel", "c", description="Game channel", flag=False, default="overseas"), + option("force", "f", description="Force the command to run"), + option( + "game-path", + "g", + description="Path to the game installation", + flag=False, + default=".", + ), + option("patch-type", "p", description="Patch type", flag=False), + option("temporary-path", "t", description="Temporary path", flag=False), + option("silent", "s", description="Silent mode"), + option("noconfirm", "y", description="Do not ask for confirmation (yes to all)"), +] + + +class State: + game: GameABC = None + + +def callback( + command: Command, +): + """ + Base callback for all commands + """ + game_path = command.option("game-path") + channel = command.option("channel") + silent = command.option("silent") + noconfirm = command.option("noconfirm") + temporary_path = command.option("temporary-path") + if isinstance(channel, str): + channel = GameChannel[channel.capitalize()] + elif isinstance(channel, int): + channel = GameChannel(channel) + if temporary_path: + paths.set_base_path(temporary_path) + if command.name.startswith("hsr"): + State.game = HSRGame(game_path, temporary_path) + patch_type = command.option("patch-type") + if patch_type is None: + patch_type = HSRPatchType.Jadeite + elif isinstance(patch_type, str): + patch_type = HSRPatchType[patch_type] + elif isinstance(patch_type, int): + patch_type = HSRPatchType(patch_type) + patcher.patch_type = patch_type + elif command.name.startswith("genshin"): + State.game = GenshinGame(game_path) + if channel: + State.game.channel_override = channel + utils.silent_message = silent + if noconfirm: + utils.no_confirm = noconfirm + + def confirm( + question: str, default: bool = False, true_answer_regex: str = r"(?i)^y" + ): + command.line( + f"{question} (yes/no) [{'yes' if default else 'no'}] y" + ) + return True + + command.confirm = confirm + command.add_style("warn", fg="yellow") + + +def set_version_config(self: Command): + self.line("Setting version config... ") + try: + State.game.set_version_config() + except Exception as e: + self.line_error(f"Couldn't set version config: {e}") + self.line_error( + "This won't affect the overall experience, but if you're using the official launcher" + ) + self.line_error( + "you may have to edit the file 'config.ini' manually to reflect the latest version." + ) + self.line( + f"The game has been updated to version: {State.game.get_version_str()}" + ) + + +class VoicepackListInstalled(Command): + name = "hsr voicepack list-installed" + description = "Get the installed voicepacks" + options = default_options + + def handle(self): + callback(command=self) + installed_voicepacks_str = [ + f"{x.name}" + for x in State.game.get_installed_voicepacks() + ] + self.line(f"Installed voicepacks: {', '.join(installed_voicepacks_str)}") + + +class VoicepackList(Command): + name = "hsr voicepack list" + description = "Get all available voicepacks" + options = default_options + [ + option("pre-download", description="Pre-download the game if available"), + ] + + def handle(self): + callback(command=self) + pre_download = self.option("pre-download") + remote_game = State.game.get_remote_game(pre_download=pre_download) + available_voicepacks_str = [ + f"{x.language.name} ({x.language.value})" + for x in remote_game.latest.voice_packs + ] + self.line(f"Available voicepacks: {', '.join(available_voicepacks_str)}") + + +class VoicepackInstall(Command): + name = "hsr voicepack install" + description = ( + "Installs the specified installed voicepacks" + ) + options = default_options + [ + option("pre-download", description="Pre-download the game if available"), + ] + arguments = [ + argument( + "language", description="Languages to install", multiple=True, optional=True + ) + ] + + def handle(self): + callback(command=self) + pre_download = self.option("pre-download") + # Typing manually because pylance detect it as Any + languages: list[str] = self.argument("language") + # Get installed voicepacks + language_objects = [] + for language in languages: + language = language.lower() + try: + language_objects.append(VoicePackLanguage[language.capitalize()]) + except KeyError: + try: + language_objects.append(VoicePackLanguage.from_remote_str(language)) + except ValueError: + self.line_error(f"Invalid language: {language}") + if len(language_objects) == 0: + self.line_error( + "No valid languages specified, you must specify a language to install" + ) + return + progress = utils.ProgressIndicator(self) + progress.start("Fetching install package information... ") + try: + game_info = State.game.get_remote_game(pre_download=pre_download) + except Exception as e: + progress.finish( + f"Fetching failed with following error: {e} \n{traceback.format_exc()}" + ) + return + progress.finish( + "Installation information fetched successfully." + ) + if not self.confirm("Do you want to install the specified voicepacks?"): + self.line("Installation aborted.") + return + # Voicepack update + for remote_voicepack in game_info.major.audio_pkgs: + if remote_voicepack.language not in language_objects: + continue + self.line( + f"Downloading install package for language: {remote_voicepack.language.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 + ) + except Exception as e: + self.line_error(f"Couldn't download package: {e}") + return + if not download_result: + self.line_error("Download failed.") + return + self.line("Download completed.") + progress = utils.ProgressIndicator(self) + progress.start("Installing package...") + try: + State.game.install_archive(archive_file) + except Exception as e: + progress.finish( + f"Couldn't apply package: {e} \n{traceback.format_exc()}" + ) + return + progress.finish( + f"Package applied for language {remote_voicepack.language.name}." + ) + self.line( + f"The voicepacks have been installed to version: {State.game.get_version_str()}" + ) + + +class VoicepackUpdate(Command): + name = "hsr voicepack update" + description = ( + "Updates the specified installed voicepacks, if not specified, updates all" + ) + options = default_options + [ + option( + "auto-repair", "R", description="Automatically repair the game if needed" + ), + option("pre-download", description="Pre-download the game if available"), + option( + "from-version", description="Update from a specific version", flag=False + ), + ] + arguments = [ + argument( + "language", description="Languages to update", multiple=True, optional=True + ) + ] + + def handle(self): + callback(command=self) + auto_repair = self.option("auto-repair") + pre_download = self.option("pre-download") + from_version = self.option("from-version") + # Typing manually because pylance detect it as Any + languages: list[str] = self.argument("language") + if auto_repair: + self.line("Auto-repair is enabled.") + if from_version: + self.line(f"Updating from version: {from_version}") + State.game.version_override = from_version + # Get installed voicepacks + if len(languages) == 0: + self.line( + "No languages specified, updating all installed voicepacks..." + ) + installed_voicepacks = State.game.get_installed_voicepacks() + if len(languages) > 0: + languages = [x.lower() for x in languages] + # Support both English and en-us and en + installed_voicepacks = [ + x + for x in installed_voicepacks + if x.name.lower() in languages + or x.value.lower() in languages + or x.name.lower()[:2] in languages + ] + installed_voicepacks_str = [ + f"{str(x.name)}" for x in installed_voicepacks + ] + self.line(f"Updating voicepacks: {', '.join(installed_voicepacks_str)}") + progress = utils.ProgressIndicator(self) + progress.start("Checking for updates... ") + try: + update_diff = 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( + f"Update checking failed with following error: {e} \n{traceback.format_exc()}" + ) + return + if update_diff is None: + progress.finish("Game is already updated.") + return + progress.finish("Update available.") + self.line( + f"The current version is: {State.game.get_version_str()}" + ) + self.line( + f"The latest version is: {game_info.latest.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: + 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(remote_voicepack.name) + try: + download_result = utils.download( + remote_voicepack.path, archive_file, file_len=update_diff.size + ) + except Exception as e: + self.line_error(f"Couldn't download update: {e}") + return + if not download_result: + self.line_error("Download failed.") + return + self.line("Download completed.") + progress = utils.ProgressIndicator(self) + progress.start("Applying update package...") + try: + State.game.apply_update_archive( + archive_file=archive_file, auto_repair=auto_repair + ) + except Exception as e: + progress.finish( + f"Couldn't apply update: {e} \n{traceback.format_exc()}" + ) + return + progress.finish( + f"Update applied for language {remote_voicepack.language.name}." + ) + set_version_config(self=self) + self.line( + f"The game has been updated to version: {State.game.get_version_str()}" + ) + + +class PatchTypeCommand(Command): + name = "hsr patch type" + description = "Get the patch type of the game" + options = default_options + + def handle(self): + callback(command=self) + self.line(f"Patch type: {patcher.patch_type.name}") + + +class UpdatePatchCommand(Command): + name = "hsr patch update" + description = "Updates the patch" + options = default_options + + def handle(self): + callback(command=self) + progress = utils.ProgressIndicator(self) + progress.start("Updating patch... ") + try: + patcher.update_patch() + except PatchUpdateError as e: + progress.finish( + f"Patch update failed with following error: {e} \n{traceback.format_exc()}" + ) + else: + progress.finish("Patch updated!") + + +class PatchInstallCommand(Command): + name = "hsr patch install" + description = "Installs the patch" + options = default_options + + def jadeite(self): + progress = utils.ProgressIndicator(self) + progress.start("Installing patch... ") + try: + jadeite_dir = patcher.patch_game(game=State.game) + except PatcherError as e: + progress.finish( + f"Patch installation failed with following error: {e} \n{traceback.format_exc()}" + ) + return + progress.finish("Patch installed!") + print() + exe_path = jadeite_dir.joinpath("jadeite.exe") + self.line(f"Jadeite executable is located at: {exe_path}") + self.line( + "You need to run the game using Jadeite to use the patch." + ) + self.line(f'E.g: {exe_path} "{State.game.path}"') + print() + self.line( + "To activate the experimental patching method, set the environment variable BREAK_CATHACK=1" + ) + self.line( + "Read more about it here: https://codeberg.org/mkrsym1/jadeite/issues/37" + ) + print() + self.line( + "Please don't spread this project to public, we just want to play the game." + ) + self.line( + "And for your own sake, please only use test accounts, as there is an extremely high risk of getting banned." + ) + + def astra(self): + progress = utils.ProgressIndicator(self) + progress.start("Installing patch... ") + try: + patcher.patch_game(game=State.game) + except PatcherError as e: + progress.finish( + f"Patch installation failed with following error: {e} \n{traceback.format_exc()}" + ) + return + progress.finish("Patch installed!") + self.line() + self.line( + "Please don't spread this project to public, we just want to play the game." + ) + self.line( + "And for your own sake, please only use testing accounts, as there is an extremely high risk of getting banned." + ) + + def handle(self): + callback(command=self) + if system() == "Windows": + self.line( + "Windows is officialy supported by the game, so no patching is needed." + ) + self.line( + "By patching the game, you are violating the ToS of the game." + ) + if not self.confirm("Do you want to patch the game?"): + self.line("Patching aborted.") + return + progress = utils.ProgressIndicator(self) + progress.start("Checking telemetry hosts... ") + telemetry_list = patcher.check_telemetry() + if telemetry_list: + progress.finish("Telemetry hosts were found.") + self.line("Below is the list of telemetry hosts that need to be blocked:") + print() + for host in telemetry_list: + self.line(f"{host}") + print() + self.line( + "To prevent the game from sending data about the patch, " + + "we need to block these hosts." + ) + if not self.confirm("Do you want to block them?"): + self.line("Patching aborted.") + self.line( + "Please block these hosts manually then try again." + ) + return + try: + patcher.block_telemetry(telemetry_list=telemetry_list) + except Exception: + self.line_error( + f"Couldn't block telemetry hosts: {traceback.format_exc()}" + ) + # There's a good reason for this. + if system() != "Windows": + self.line( + "Cannot continue, please block them manually then try again." + ) + return + self.line("Continuing anyway...") + else: + progress.finish("No telemetry hosts found.") + progress = utils.ProgressIndicator(self) + progress.start("Updating patch... ") + try: + patcher.update_patch() + except PatchUpdateError as e: + progress.finish( + f"Patch update failed with following error: {e} \n{traceback.format_exc()}" + ) + else: + progress.finish("Patch updated.") + match patcher.patch_type: + case HSRPatchType.Jadeite: + self.jadeite() + case HSRPatchType.Astra: + self.astra() + + +PatchCommand = copy.deepcopy(PatchInstallCommand) +PatchCommand.name = "hsr patch" + + +class PatchTelemetryCommand(Command): + name = "hsr patch telemetry" + description = "Checks for telemetry hosts and block them." + options = default_options + + def handle(self): + progress = utils.ProgressIndicator(self) + progress.start("Checking telemetry hosts... ") + telemetry_list = patcher.check_telemetry() + if telemetry_list: + progress.finish("Telemetry hosts were found.") + self.line("Below is the list of telemetry hosts that need to be blocked:") + print() + for host in telemetry_list: + self.line(f"{host}") + print() + self.line( + "To prevent the game from sending data about the patch, " + + "we need to block these hosts." + ) + if not self.confirm("Do you want to block them?"): + self.line("Blocking aborted.") + return + try: + patcher.block_telemetry(telemetry_list=telemetry_list) + except Exception: + self.line_error( + f"Couldn't block telemetry hosts: {traceback.format_exc()}" + ) + else: + progress.finish("No telemetry hosts found.") + + +class GetVersionCommand(Command): + name = "hsr version" + description = "Gets the local game version" + options = default_options + + def handle(self): + callback(command=self) + try: + self.line( + f"Version: {'.'.join(str(x) for x in State.game.get_version())}" + ) + except GameError as e: + self.line_error(f"Couldn't get game version: {e}") + + +class InstallCommand(Command): + name = "hsr install" + description = ( + "Installs the latest version of the game to the specified path (default: current directory). " + + "Note that this will not install the default voicepack (English), you need to install it manually." + ) + options = default_options + [ + option("pre-download", description="Pre-download the game if available"), + ] + + def handle(self): + callback(command=self) + pre_download = self.option("pre-download") + progress = utils.ProgressIndicator(self) + progress.start("Fetching install package information... ") + try: + game_info = State.game.get_remote_game(pre_download=pre_download) + except Exception as e: + progress.finish( + f"Fetching failed with following error: {e} \n{traceback.format_exc()}" + ) + return + progress.finish( + "Installation information fetched successfully." + ) + if not self.confirm("Do you want to install the game?"): + self.line("Installation aborted.") + return + self.line("Downloading install package...") + first_pkg_out_path = None + for game_pkg in game_info.major.game_pkgs: + out_path = State.game.cache.joinpath(PurePath(game_pkg.url).name) + if not first_pkg_out_path: + first_pkg_out_path = out_path + try: + download_result = utils.download( + game_pkg.url, out_path, file_len=game_pkg.size + ) + except Exception as e: + self.line_error( + f"Couldn't download install package: {e}" + ) + return + if not download_result: + self.line_error("Download failed.") + return + self.line("Download completed.") + progress = utils.ProgressIndicator(self) + progress.start("Installing package...") + try: + State.game.install_archive(first_pkg_out_path) + except Exception as e: + progress.finish( + f"Couldn't install package: {e} \n{traceback.format_exc()}" + ) + return + progress.finish("Package applied for the base game.") + self.line("Setting version config... ") + State.game.version_override = game_info.major.version + set_version_config() + State.game.version_override = None + self.line( + f"The game has been installed to version: {State.game.get_version_str()}" + ) + + +class UpdateCommand(Command): + name = "hsr update" + description = "Updates the local game if available" + options = default_options + [ + option( + "auto-repair", "R", description="Automatically repair the game if needed" + ), + option("pre-download", description="Pre-download the game if available"), + option( + "from-version", description="Update from a specific version", flag=False + ), + ] + + def handle(self): + callback(command=self) + auto_repair = self.option("auto-repair") + pre_download = self.option("pre-download") + from_version = self.option("from-version") + if auto_repair: + self.line("Auto-repair is enabled.") + if from_version: + self.line(f"Updating from version: {from_version}") + State.game.version_override = from_version + progress = utils.ProgressIndicator(self) + progress.start("Checking for updates... ") + try: + update_diff = 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( + f"Update checking failed with following error: {e} \n{traceback.format_exc()}" + ) + return + if update_diff is None or isinstance(game_info.major, str | None): + progress.finish("Game is already updated.") + return + progress.finish("Update available.") + self.line( + f"The current version is: {State.game.get_version_str()}" + ) + self.line( + 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 + self.line("Downloading update package...") + update_game_url = update_diff.game_pkgs[0].url + out_path = State.game.cache.joinpath(PurePath(update_game_url).name) + try: + download_result = utils.download( + update_game_url, out_path, file_len=update_diff.game_pkgs[0].size + ) + except Exception as e: + self.line_error(f"Couldn't download update: {e}") + return + + if not download_result: + self.line_error("Download failed.") + return + self.line("Download completed.") + progress = utils.ProgressIndicator(self) + progress.start("Applying update package...") + try: + State.game.apply_update_archive(out_path, auto_repair=auto_repair) + except Exception as e: + progress.finish( + f"Couldn't apply update: {e} \n{traceback.format_exc()}" + ) + return + progress.finish("Update applied for base game.") + # Get installed voicepacks + installed_voicepacks = State.game.get_installed_voicepacks() + # Voicepack update + for remote_voicepack in update_diff.audio_pkgs: + if remote_voicepack.language not in installed_voicepacks: + continue + # Voicepack is installed, update it + archive_file = State.game.cache.joinpath( + PurePath(remote_voicepack.url).name + ) + self.line( + f"Downloading update package for voicepack language '{remote_voicepack.language.name}'..." + ) + try: + download_result = utils.download( + remote_voicepack.url, archive_file, file_len=remote_voicepack.size + ) + except Exception as e: + self.line_error(f"Couldn't download update: {e}") + return + if not download_result: + self.line_error("Download failed.") + return + self.line("Download completed.") + progress = utils.ProgressIndicator(self) + progress.start("Applying update package...") + try: + State.game.apply_update_archive( + archive_file=archive_file, auto_repair=auto_repair + ) + except Exception as e: + progress.finish( + f"Couldn't apply update: {e} \n{traceback.format_exc()}" + ) + return + progress.finish( + f"Update applied for language {remote_voicepack.language.name}." + ) + self.line("Setting version config... ") + State.game.version_override = game_info.major.version + set_version_config() + State.game.version_override = None + self.line( + f"The game has been updated to version: {State.game.get_version_str()}" + ) + + +class RepairCommand(Command): + name = "hsr repair" + description = "Tries to repair the local game" + options = default_options + + def handle(self): + callback(command=self) + self.line( + "This command will try to repair the game by downloading missing/broken files." + ) + self.line( + "There will be no progress available, so please be patient and just wait." + ) + if not self.confirm( + "Do you want to repair the game (this will take a long time!)?" + ): + self.line("Repairation aborted.") + return + progress = utils.ProgressIndicator(self) + progress.start("Repairing game files (no progress available)... ") + try: + State.game.repair_game() + except Exception as e: + progress.finish( + f"Repairation failed with following error: {e} \n{traceback.format_exc()}" + ) + return + progress.finish("Repairation completed.") + + +class InstallDownloadCommand(Command): + name = "hsr install download" + description = ( + "Downloads the latest version of the game. " + + "Note that this will not download the default voicepack (English), you need to download it manually." + ) + options = default_options + [ + option("pre-download", description="Pre-download the game if available"), + ] + + def handle(self): + callback(command=self) + pre_download = self.option("pre-download") + progress = utils.ProgressIndicator(self) + progress.start("Fetching install package information... ") + try: + game_info = State.game.get_remote_game(pre_download=pre_download) + except Exception as e: + progress.finish( + f"Fetching failed with following error: {e} \n{traceback.format_exc()}" + ) + return + progress.finish( + "Installation information fetched successfully." + ) + if not self.confirm("Do you want to download the game?"): + self.line("Download aborted.") + return + self.line("Downloading install package...") + first_pkg_out_path = None + for game_pkg in game_info.major.game_pkgs: + out_path = State.game.cache.joinpath(PurePath(game_pkg.url).name) + if not first_pkg_out_path: + first_pkg_out_path = out_path + try: + download_result = utils.download( + game_pkg.url, out_path, file_len=game_pkg.size + ) + except Exception as e: + self.line_error( + f"Couldn't download install package: {e}" + ) + return + if not download_result: + self.line_error("Download failed.") + return + self.line("Download completed.") + + +class UpdateDownloadCommand(Command): + name = "hsr update download" + description = "Download the update for the local game if available" + options = default_options + [ + option( + "auto-repair", "R", description="Automatically repair the game if needed" + ), + option("pre-download", description="Pre-download the game if available"), + option( + "from-version", description="Update from a specific version", flag=False + ), + ] + + def handle(self): + callback(command=self) + auto_repair = self.option("auto-repair") + pre_download = self.option("pre-download") + from_version = self.option("from-version") + if auto_repair: + self.line("Auto-repair is enabled.") + if from_version: + self.line(f"Updating from version: {from_version}") + State.game.version_override = from_version + progress = utils.ProgressIndicator(self) + progress.start("Checking for updates... ") + try: + update_diff = 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( + f"Update checking failed with following error: {e} \n{traceback.format_exc()}" + ) + return + if update_diff is None or isinstance(game_info.major, str | None): + progress.finish("Game is already updated.") + return + progress.finish("Update available.") + self.line( + f"The current version is: {State.game.get_version_str()}" + ) + self.line( + f"The latest version is: {game_info.major.version}" + ) + if not self.confirm("Do you want to download the update?"): + self.line("Download aborted.") + return + self.line("Downloading update package...") + update_game_url = update_diff.game_pkgs[0].url + out_path = State.game.cache.joinpath(PurePath(update_game_url).name) + try: + download_result = utils.download( + update_game_url, out_path, file_len=update_diff.game_pkgs[0].size + ) + except Exception as e: + self.line_error( + f"Couldn't download update: {e} \n{traceback.format_exc()}" + ) + return + + if not download_result: + self.line_error("Download failed.") + return + self.line("Download completed.") + # Get installed voicepacks + installed_voicepacks = State.game.get_installed_voicepacks() + # Voicepack update + for remote_voicepack in update_diff.audio_pkgs: + if remote_voicepack.language not in installed_voicepacks: + continue + # Voicepack is installed, update it + archive_file = State.game.cache.joinpath( + PurePath(remote_voicepack.url).name + ) + self.line( + f"Downloading update package for voicepack language '{remote_voicepack.language.name}'..." + ) + try: + download_result = utils.download( + remote_voicepack.url, archive_file, file_len=remote_voicepack.size + ) + except Exception as e: + self.line_error(f"Couldn't download update: {e}") + return + if not download_result: + self.line_error("Download failed.") + return + self.line("Download completed.") + + +class ApplyInstallArchive(Command): + name = "hsr install apply-archive" + description = "Applies the install archive" + arguments = [argument("path", description="Path to the install archive")] + options = default_options + + def handle(self): + callback(command=self) + install_archive = self.argument("path") + progress = utils.ProgressIndicator(self) + progress.start("Applying install package...") + try: + State.game.install_archive(install_archive) + except Exception as e: + progress.finish( + f"Couldn't apply package: {e} \n{traceback.format_exc()}" + ) + return + progress.finish("Package applied.") + set_version_config(self=self) + + +class ApplyUpdateArchive(Command): + name = "hsr update apply-archive" + description = "Applies the update archive to the local game" + arguments = [argument("path", description="Path to the update archive")] + options = default_options + [ + option( + "auto-repair", "R", description="Automatically repair the game if needed" + ), + ] + + def handle(self): + callback(command=self) + update_archive = self.argument("path") + auto_repair = self.option("auto-repair") + progress = utils.ProgressIndicator(self) + progress.start("Applying update package...") + try: + State.game.apply_update_archive(update_archive, auto_repair=auto_repair) + except Exception as e: + progress.finish( + f"Couldn't apply update: {e} \n{traceback.format_exc()}" + ) + return + progress.finish("Update applied.") + set_version_config() + +# This is the list for HSR commands, we'll add Genshin commands later +hsr_exports = [ + ApplyInstallArchive, + ApplyUpdateArchive, + GetVersionCommand, + InstallCommand, + InstallDownloadCommand, + PatchCommand, + PatchInstallCommand, + PatchTelemetryCommand, + PatchTypeCommand, + RepairCommand, + UpdatePatchCommand, + UpdateCommand, + UpdateDownloadCommand, + VoicepackInstall, + VoicepackList, + VoicepackListInstalled, + VoicepackUpdate, +] +exports = [] +for command in hsr_exports: + exports.append(command) + if "patch" in command.name: + continue + new_command = copy.deepcopy(command) + new_command.name = f"genshin {new_command.name[4:]}" + exports.append(new_command) diff --git a/vollerei/cli/genshin.py b/vollerei/cli/genshin.py index 239a583..3d32494 100644 --- a/vollerei/cli/genshin.py +++ b/vollerei/cli/genshin.py @@ -10,19 +10,18 @@ from vollerei import paths default_options = [ - option("channel", "c", description="Game channel", flag=False, default="overseas"), - option("force", "f", description="Force the command to run"), + option("channel", "c", description="Game channel.", flag=False, default="overseas"), + option("force", "f", description="Force the command to run."), option( "game-path", "g", - description="Path to the game installation", + description="Path to the game installation.", flag=False, default=".", ), - option("patch-type", "p", description="Patch type", flag=False), - option("temporary-path", "t", description="Temporary path", flag=False), - option("silent", "s", description="Silent mode"), - option("noconfirm", "y", description="Do not ask for confirmation (yes to all)"), + option("temporary-path", "t", description="Temporary path.", flag=False), + option("silent", "s", description="Silent mode."), + option("noconfirm", "y", description="Do not ask for confirmation (yes to all)."), ] @@ -85,7 +84,7 @@ def set_version_config(self: Command): class VoicepackListInstalled(Command): name = "genshin voicepack list-installed" - description = "Get the installed voicepacks" + description = "Gets the installed voicepacks." options = default_options def handle(self): @@ -99,9 +98,9 @@ class VoicepackListInstalled(Command): class VoicepackList(Command): name = "genshin voicepack list" - description = "Get all available voicepacks" + description = "Gets all available voicepacks." options = default_options + [ - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), ] def handle(self): @@ -118,10 +117,10 @@ class VoicepackList(Command): class VoicepackInstall(Command): name = "genshin voicepack install" description = ( - "Installs the specified installed voicepacks" + "Installs the specified installed voicepacks." ) options = default_options + [ - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), ] arguments = [ argument( @@ -204,13 +203,13 @@ class VoicepackInstall(Command): class VoicepackUpdate(Command): name = "genshin voicepack update" description = ( - "Updates the specified installed voicepacks, if not specified, updates all" + "Updates the specified installed voicepacks, if not specified, updates all." ) options = default_options + [ option( - "auto-repair", "R", description="Automatically repair the game if needed" + "auto-repair", "R", description="Automatically repair the game if needed." ), - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), option( "from-version", description="Update from a specific version", flag=False ), @@ -318,7 +317,7 @@ class VoicepackUpdate(Command): class GetVersionCommand(Command): name = "genshin version" - description = "Gets the local game version" + description = "Gets the local game version." options = default_options def handle(self): @@ -338,7 +337,7 @@ class InstallCommand(Command): + "Note that this will not install the default voicepack (English), you need to install it manually." ) options = default_options + [ - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), ] def handle(self): @@ -402,9 +401,9 @@ class UpdateCommand(Command): description = "Updates the local game if available" options = default_options + [ option( - "auto-repair", "R", description="Automatically repair the game if needed" + "auto-repair", "R", description="Automatically repair the game if needed." ), - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), option( "from-version", description="Update from a specific version", flag=False ), @@ -552,7 +551,7 @@ class InstallDownloadCommand(Command): + "Note that this will not download the default voicepack (English), you need to download it manually." ) options = default_options + [ - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), ] def handle(self): @@ -599,9 +598,9 @@ class UpdateDownloadCommand(Command): description = "Download the update for the local game if available" options = default_options + [ option( - "auto-repair", "R", description="Automatically repair the game if needed" + "auto-repair", "R", description="Automatically repair the game if needed." ), - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), option( "from-version", description="Update from a specific version", flag=False ), @@ -711,7 +710,7 @@ class ApplyUpdateArchive(Command): arguments = [argument("path", description="Path to the update archive")] options = default_options + [ option( - "auto-repair", "R", description="Automatically repair the game if needed" + "auto-repair", "R", description="Automatically repair the game if needed." ), ] diff --git a/vollerei/cli/hsr.py b/vollerei/cli/hsr.py index e29e615..520e833 100644 --- a/vollerei/cli/hsr.py +++ b/vollerei/cli/hsr.py @@ -115,7 +115,7 @@ class VoicepackList(Command): name = "hsr voicepack list" description = "Get all available voicepacks" options = default_options + [ - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), ] def handle(self): @@ -135,7 +135,7 @@ class VoicepackInstall(Command): "Installs the specified installed voicepacks" ) options = default_options + [ - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), ] arguments = [ argument( @@ -222,9 +222,9 @@ class VoicepackUpdate(Command): ) options = default_options + [ option( - "auto-repair", "R", description="Automatically repair the game if needed" + "auto-repair", "R", description="Automatically repair the game if needed." ), - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), option( "from-version", description="Update from a specific version", flag=False ), @@ -539,7 +539,7 @@ class InstallCommand(Command): + "Note that this will not install the default voicepack (English), you need to install it manually." ) options = default_options + [ - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), ] def handle(self): @@ -603,9 +603,9 @@ class UpdateCommand(Command): description = "Updates the local game if available" options = default_options + [ option( - "auto-repair", "R", description="Automatically repair the game if needed" + "auto-repair", "R", description="Automatically repair the game if needed." ), - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), option( "from-version", description="Update from a specific version", flag=False ), @@ -753,7 +753,7 @@ class InstallDownloadCommand(Command): + "Note that this will not download the default voicepack (English), you need to download it manually." ) options = default_options + [ - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), ] def handle(self): @@ -800,9 +800,9 @@ class UpdateDownloadCommand(Command): description = "Download the update for the local game if available" options = default_options + [ option( - "auto-repair", "R", description="Automatically repair the game if needed" + "auto-repair", "R", description="Automatically repair the game if needed." ), - option("pre-download", description="Pre-download the game if available"), + option("pre-download", description="Pre-download the game if available."), option( "from-version", description="Update from a specific version", flag=False ), @@ -912,7 +912,7 @@ class ApplyUpdateArchive(Command): arguments = [argument("path", description="Path to the update archive")] options = default_options + [ option( - "auto-repair", "R", description="Automatically repair the game if needed" + "auto-repair", "R", description="Automatically repair the game if needed." ), ]