Compare commits

..

3 Commits

Author SHA1 Message Date
ca03718bf1 chore: rename _path to path 2024-01-28 18:26:48 +07:00
9f9a63c9b0 feat(hsr): implement voicepack update 2024-01-28 17:11:16 +07:00
4c851b999a fix(hsr): proper implement --noconfirm 2024-01-25 20:18:56 +07:00
5 changed files with 144 additions and 50 deletions

View File

@ -10,6 +10,7 @@ class GameABC(ABC):
""" """
path: Path path: Path
cache: Path
version_override: tuple[int, int, int] | None version_override: tuple[int, int, int] | None
channel_override: Any channel_override: Any
@ -83,12 +84,6 @@ class GameABC(ABC):
""" """
pass pass
def get_voiceover_update(self, language: str):
"""
Get the voiceover update
"""
pass
def get_channel(self): def get_channel(self):
""" """
Get the game channel Get the game channel

View File

@ -25,7 +25,7 @@ default_options = [
option("patch-type", "p", description="Patch type", flag=False), option("patch-type", "p", description="Patch type", flag=False),
option("temporary-path", "t", description="Temporary path", flag=False), option("temporary-path", "t", description="Temporary path", flag=False),
option("silent", "s", description="Silent mode"), option("silent", "s", description="Silent mode"),
option("noconfirm", "y", description="Do not ask for confirmation"), option("noconfirm", "y", description="Do not ask for confirmation (yes to all)"),
] ]
@ -62,6 +62,16 @@ def callback(
utils.silent_message = silent utils.silent_message = silent
if noconfirm: if noconfirm:
utils.no_confirm = noconfirm utils.no_confirm = noconfirm
def confirm(
question: str, default: bool = False, true_answer_regex: str = r"(?i)^y"
):
command.line(
f"<question>{question} (yes/no)</question> [<comment>{'yes' if default else 'no'}</comment>] y"
)
return True
command.confirm = confirm
command.add_style("warn", fg="yellow") command.add_style("warn", fg="yellow")
@ -116,8 +126,13 @@ class PatchInstallCommand(Command):
self.line( self.line(
"You need to <warn>run the game using Jadeite</warn> to use the patch." "You need to <warn>run the game using Jadeite</warn> to use the patch."
) )
self.line(f'E.g: <question>{exe_path} "{State.game.path}"</question>')
print()
self.line( self.line(
f'E.g: <question>I_WANT_A_BAN=1 {exe_path} "{State.game.path}"</question>' "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() print()
self.line( self.line(
@ -302,7 +317,7 @@ class UpdateCommand(Command):
self.line("<error>Update aborted.</error>") self.line("<error>Update aborted.</error>")
return return
self.line("Downloading update package...") self.line("Downloading update package...")
out_path = State.game._cache.joinpath(update_diff.name) out_path = State.game.cache.joinpath(update_diff.name)
try: try:
download_result = utils.download( download_result = utils.download(
update_diff.path, out_path, file_len=update_diff.size update_diff.path, out_path, file_len=update_diff.size
@ -324,7 +339,40 @@ class UpdateCommand(Command):
f"<error>Couldn't apply update: {e} ({e.__context__})</error>" f"<error>Couldn't apply update: {e} ({e.__context__})</error>"
) )
return return
progress.finish("<comment>Update applied.</comment>") progress.finish("<comment>Update applied for base game.</comment>")
# Get installed voicepacks
installed_voicepacks = State.game.get_installed_voicepacks()
# Voicepack update
for remote_voicepack in update_diff.voice_packs:
if remote_voicepack.language not in installed_voicepacks:
continue
# Voicepack is installed, update it
archive_file = State.game.cache.joinpath(remote_voicepack.name)
try:
download_result = utils.download(
update_diff.path, archive_file, file_len=update_diff.size
)
except Exception as e:
self.line_error(f"<error>Couldn't download update: {e}</error>")
return
if not download_result:
self.line_error("<error>Download failed.</error>")
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"<error>Couldn't apply update: {e} ({e.__context__})</error>"
)
return
progress.finish(
f"<comment>Update applied for language {remote_voicepack.language}.</comment>"
)
self.line("Setting version config... ") self.line("Setting version config... ")
self.set_version_config() self.set_version_config()
self.line( self.line(

View File

@ -47,6 +47,7 @@ class ProgressIndicator:
interval=interval, values=values interval=interval, values=values
) )
self.thread = Thread(target=self.auto_advance) self.thread = Thread(target=self.auto_advance)
self.thread.daemon = True
def start(self, message: str): def start(self, message: str):
""" """

View File

@ -67,7 +67,7 @@ def apply_update_archive(
# Patch function # Patch function
def extract_and_patch(file, patch_file): def extract_and_patch(file, patch_file):
patchpath = game._cache.joinpath(patch_file) patchpath = game.cache.joinpath(patch_file)
# Delete old patch file if exists # Delete old patch file if exists
patchpath.unlink(missing_ok=True) patchpath.unlink(missing_ok=True)
# Extract patch file # Extract patch file

View File

@ -1,3 +1,4 @@
from configparser import ConfigParser
from hashlib import md5 from hashlib import md5
from io import IOBase from io import IOBase
from os import PathLike from os import PathLike
@ -5,6 +6,7 @@ from pathlib import Path
from vollerei.abc.launcher.game import GameABC from vollerei.abc.launcher.game import GameABC
from vollerei.common import ConfigFile, functions from vollerei.common import ConfigFile, functions
from vollerei.common.api import resource from vollerei.common.api import resource
from vollerei.common.enums import VoicePackLanguage
from vollerei.exceptions.game import ( from vollerei.exceptions.game import (
GameAlreadyUpdatedError, GameAlreadyUpdatedError,
GameNotInstalledError, GameNotInstalledError,
@ -32,8 +34,8 @@ class Game(GameABC):
if not cache_path: if not cache_path:
cache_path = paths.cache_path cache_path = paths.cache_path
cache_path = Path(cache_path) cache_path = Path(cache_path)
self._cache: Path = cache_path.joinpath("game/hsr/") self.cache: Path = cache_path.joinpath("game/hsr/")
self._cache.mkdir(parents=True, exist_ok=True) self.cache.mkdir(parents=True, exist_ok=True)
self._version_override: tuple[int, int, int] | None = None self._version_override: tuple[int, int, int] | None = None
self._channel_override: GameChannel | None = None self._channel_override: GameChannel | None = None
@ -111,6 +113,42 @@ class Game(GameABC):
return False return False
return True return True
def get_channel(self) -> GameChannel:
"""
Gets the current game channel.
Only works for Star Rail version 1.0.5, other versions will return the
overridden channel or GameChannel.Overseas if no channel is overridden.
This is not needed for game patching, since the patcher will automatically
detect the channel.
Returns:
GameChannel: The current game channel.
"""
version = self._version_override or self.get_version()
if version == (1, 0, 5):
for channel, v in MD5SUMS["1.0.5"].values():
for file, md5sum in v.values():
if (
md5(self._path.joinpath(file).read_bytes()).hexdigest()
!= md5sum
):
continue
match channel:
case "cn":
return GameChannel.China
case "os":
return GameChannel.Overseas
else:
# if self._path.joinpath("StarRail_Data").is_dir():
# return GameChannel.Overseas
# elif self._path.joinpath("StarRail_Data").exists():
# return GameChannel.China
# No reliable method there, so we'll just return the overridden channel or
# fallback to overseas.
return self._channel_override or GameChannel.Overseas
def get_version_config(self) -> tuple[int, int, int]: def get_version_config(self) -> tuple[int, int, int]:
""" """
Gets the current installed game version from config.ini. Gets the current installed game version from config.ini.
@ -147,11 +185,24 @@ class Game(GameABC):
This method is meant to keep compatibility with the official launcher only. This method is meant to keep compatibility with the official launcher only.
""" """
cfg_file = self._path.joinpath("config.ini") cfg_file = self._path.joinpath("config.ini")
if not cfg_file.exists(): if cfg_file.exists():
raise FileNotFoundError("config.ini not found.") cfg = ConfigFile(cfg_file)
cfg = ConfigFile(cfg_file) cfg.set("General", "game_version", self.get_version_str())
cfg.set("General", "game_version", self.get_version_str()) cfg.save()
cfg.save() else:
cfg = ConfigParser()
cfg.read_dict(
{
"General": {
"channel": 1,
"cps": "hoyoverse_PC",
"game_version": self.get_version_str(),
"sub_channel": 1,
"plugin_2_version": "0.0.1",
}
}
)
cfg.write(cfg_file.open("w"))
def get_version(self) -> tuple[int, int, int]: def get_version(self) -> tuple[int, int, int]:
""" """
@ -232,41 +283,27 @@ class Game(GameABC):
""" """
return ".".join(str(i) for i in self.get_version()) return ".".join(str(i) for i in self.get_version())
def get_channel(self) -> GameChannel: def get_installed_voicepacks(self) -> list[VoicePackLanguage]:
""" """
Gets the current game channel. Gets the installed voicepacks.
Only works for Star Rail version 1.0.5, other versions will return the
overridden channel or GameChannel.Overseas if no channel is overridden.
This is not needed for game patching, since the patcher will automatically
detect the channel.
Returns: Returns:
GameChannel: The current game channel. list[VoicePackLanguage]: A list of installed voicepacks.
""" """
version = self._version_override or self.get_version() if not self.is_installed():
if version == (1, 0, 5): raise GameNotInstalledError("Game is not installed.")
for channel, v in MD5SUMS["1.0.5"].values(): voicepacks = []
for file, md5sum in v.values(): for child in (
if ( self.data_folder()
md5(self._path.joinpath(file).read_bytes()).hexdigest() .joinpath("Persistent/Audio/AudioPackage/Windows/")
!= md5sum .iterdir()
): ):
continue if child.is_dir():
match channel: try:
case "cn": voicepacks.append(VoicePackLanguage(child.name))
return GameChannel.China except ValueError:
case "os": pass
return GameChannel.Overseas return voicepacks
else:
# if self._path.joinpath("StarRail_Data").is_dir():
# return GameChannel.Overseas
# elif self._path.joinpath("StarRail_Data").exists():
# return GameChannel.China
# No reliable method there, so we'll just return the overridden channel or
# fallback to overseas.
return self._channel_override or GameChannel.Overseas
def get_remote_game(self, pre_download: bool = False) -> resource.Game: def get_remote_game(self, pre_download: bool = False) -> resource.Game:
""" """
@ -393,7 +430,20 @@ class Game(GameABC):
update_info = self.get_update() update_info = self.get_update()
if not update_info or update_info.version == self.get_version_str(): if not update_info or update_info.version == self.get_version_str():
raise GameAlreadyUpdatedError("Game is already updated.") raise GameAlreadyUpdatedError("Game is already updated.")
archive_file = self._cache.joinpath(update_info.name) # Base game update
archive_file = self.cache.joinpath(update_info.name)
download(update_info.path, archive_file) download(update_info.path, archive_file)
self.apply_update_archive(archive_file=archive_file, auto_repair=auto_repair) self.apply_update_archive(archive_file=archive_file, auto_repair=auto_repair)
# Get installed voicepacks
installed_voicepacks = self.get_installed_voicepacks()
# Voicepack update
for remote_voicepack in update_info.voice_packs:
if remote_voicepack.language not in installed_voicepacks:
continue
# Voicepack is installed, update it
archive_file = self.cache.joinpath(remote_voicepack.name)
download(remote_voicepack.path, archive_file)
self.apply_update_archive(
archive_file=archive_file, auto_repair=auto_repair
)
self.set_version_config() self.set_version_config()