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

View File

@ -25,7 +25,7 @@ default_options = [
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"),
option("noconfirm", "y", description="Do not ask for confirmation (yes to all)"),
]
@ -62,6 +62,16 @@ def callback(
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>{question} (yes/no)</question> [<comment>{'yes' if default else 'no'}</comment>] y"
)
return True
command.confirm = confirm
command.add_style("warn", fg="yellow")
@ -116,8 +126,13 @@ class PatchInstallCommand(Command):
self.line(
"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(
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()
self.line(
@ -302,7 +317,7 @@ class UpdateCommand(Command):
self.line("<error>Update aborted.</error>")
return
self.line("Downloading update package...")
out_path = State.game._cache.joinpath(update_diff.name)
out_path = State.game.cache.joinpath(update_diff.name)
try:
download_result = utils.download(
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>"
)
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.set_version_config()
self.line(

View File

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

View File

@ -67,7 +67,7 @@ def apply_update_archive(
# Patch function
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
patchpath.unlink(missing_ok=True)
# Extract patch file

View File

@ -1,3 +1,4 @@
from configparser import ConfigParser
from hashlib import md5
from io import IOBase
from os import PathLike
@ -5,6 +6,7 @@ from pathlib import Path
from vollerei.abc.launcher.game import GameABC
from vollerei.common import ConfigFile, functions
from vollerei.common.api import resource
from vollerei.common.enums import VoicePackLanguage
from vollerei.exceptions.game import (
GameAlreadyUpdatedError,
GameNotInstalledError,
@ -32,8 +34,8 @@ class Game(GameABC):
if not cache_path:
cache_path = paths.cache_path
cache_path = Path(cache_path)
self._cache: Path = cache_path.joinpath("game/hsr/")
self._cache.mkdir(parents=True, exist_ok=True)
self.cache: Path = cache_path.joinpath("game/hsr/")
self.cache.mkdir(parents=True, exist_ok=True)
self._version_override: tuple[int, int, int] | None = None
self._channel_override: GameChannel | None = None
@ -111,6 +113,42 @@ class Game(GameABC):
return False
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]:
"""
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.
"""
cfg_file = self._path.joinpath("config.ini")
if not cfg_file.exists():
raise FileNotFoundError("config.ini not found.")
cfg = ConfigFile(cfg_file)
cfg.set("General", "game_version", self.get_version_str())
cfg.save()
if cfg_file.exists():
cfg = ConfigFile(cfg_file)
cfg.set("General", "game_version", self.get_version_str())
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]:
"""
@ -232,41 +283,27 @@ class Game(GameABC):
"""
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.
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.
Gets the installed voicepacks.
Returns:
GameChannel: The current game channel.
list[VoicePackLanguage]: A list of installed voicepacks.
"""
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
if not self.is_installed():
raise GameNotInstalledError("Game is not installed.")
voicepacks = []
for child in (
self.data_folder()
.joinpath("Persistent/Audio/AudioPackage/Windows/")
.iterdir()
):
if child.is_dir():
try:
voicepacks.append(VoicePackLanguage(child.name))
except ValueError:
pass
return voicepacks
def get_remote_game(self, pre_download: bool = False) -> resource.Game:
"""
@ -393,7 +430,20 @@ class Game(GameABC):
update_info = self.get_update()
if not update_info or update_info.version == self.get_version_str():
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)
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()