fix: applying update archive

It now works, previously I thought py7zr works the same as zipfile but it's different af so I had to change some code for that.
This commit is contained in:
2024-10-23 20:46:44 +07:00
parent f02f1d5988
commit 7e5a60bb33
4 changed files with 91 additions and 72 deletions

View File

@ -148,7 +148,7 @@ class VoicepackUpdateAll(Command):
game_info = State.game.get_remote_game(pre_download=pre_download) game_info = State.game.get_remote_game(pre_download=pre_download)
except Exception as e: except Exception as e:
progress.finish( progress.finish(
f"<error>Update checking failed with following error: {e} \n({traceback.format_exc()})</error>" f"<error>Update checking failed with following error: {e} \n{traceback.format_exc()}</error>"
) )
return return
if update_diff is None: if update_diff is None:
@ -192,7 +192,7 @@ class VoicepackUpdateAll(Command):
) )
except Exception as e: except Exception as e:
progress.finish( progress.finish(
f"<error>Couldn't apply update: {e} \n({traceback.format_exc()})</error>" f"<error>Couldn't apply update: {e} \n{traceback.format_exc()}</error>"
) )
return return
progress.finish( progress.finish(
@ -228,7 +228,7 @@ class UpdatePatchCommand(Command):
patcher.update_patch() patcher.update_patch()
except PatchUpdateError as e: except PatchUpdateError as e:
progress.finish( progress.finish(
f"<error>Patch update failed with following error: {e} \n({traceback.format_exc()})</error>" f"<error>Patch update failed with following error: {e} \n{traceback.format_exc()}</error>"
) )
else: else:
progress.finish("<comment>Patch updated!</comment>") progress.finish("<comment>Patch updated!</comment>")
@ -246,7 +246,7 @@ class PatchInstallCommand(Command):
jadeite_dir = patcher.patch_game(game=State.game) jadeite_dir = patcher.patch_game(game=State.game)
except PatcherError as e: except PatcherError as e:
progress.finish( progress.finish(
f"<error>Patch installation failed with following error: {e} \n({traceback.format_exc()})</error>" f"<error>Patch installation failed with following error: {e} \n{traceback.format_exc()}</error>"
) )
return return
progress.finish("<comment>Patch installed!</comment>") progress.finish("<comment>Patch installed!</comment>")
@ -279,7 +279,7 @@ class PatchInstallCommand(Command):
patcher.patch_game(game=State.game) patcher.patch_game(game=State.game)
except PatcherError as e: except PatcherError as e:
progress.finish( progress.finish(
f"<error>Patch installation failed with following error: {e} \n({traceback.format_exc()})</error>" f"<error>Patch installation failed with following error: {e} \n{traceback.format_exc()}</error>"
) )
return return
progress.finish("<comment>Patch installed!</comment>") progress.finish("<comment>Patch installed!</comment>")
@ -344,7 +344,7 @@ class PatchInstallCommand(Command):
patcher.update_patch() patcher.update_patch()
except PatchUpdateError as e: except PatchUpdateError as e:
progress.finish( progress.finish(
f"<error>Patch update failed with following error: {e} \n({traceback.format_exc()})</error>" f"<error>Patch update failed with following error: {e} \n{traceback.format_exc()}</error>"
) )
else: else:
progress.finish("<comment>Patch updated.</comment>") progress.finish("<comment>Patch updated.</comment>")
@ -437,7 +437,7 @@ class UpdateCommand(Command):
game_info = State.game.get_remote_game(pre_download=pre_download) game_info = State.game.get_remote_game(pre_download=pre_download)
except Exception as e: except Exception as e:
progress.finish( progress.finish(
f"<error>Update checking failed with following error: {e} \n({traceback.format_exc()})</error>" f"<error>Update checking failed with following error: {e} \n{traceback.format_exc()}</error>"
) )
return return
if update_diff is None or isinstance(game_info.major, str | None): if update_diff is None or isinstance(game_info.major, str | None):
@ -474,7 +474,7 @@ class UpdateCommand(Command):
State.game.apply_update_archive(out_path, auto_repair=auto_repair) State.game.apply_update_archive(out_path, auto_repair=auto_repair)
except Exception as e: except Exception as e:
progress.finish( progress.finish(
f"<error>Couldn't apply update: {e} \n({traceback.format_exc()})</error>" f"<error>Couldn't apply update: {e} \n{traceback.format_exc()}</error>"
) )
return return
progress.finish("<comment>Update applied for base game.</comment>") progress.finish("<comment>Update applied for base game.</comment>")
@ -485,7 +485,12 @@ class UpdateCommand(Command):
if remote_voicepack.language not in installed_voicepacks: if remote_voicepack.language not in installed_voicepacks:
continue continue
# Voicepack is installed, update it # Voicepack is installed, update it
archive_file = State.game.cache.joinpath(PurePath(remote_voicepack.url).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}'..."
)
try: try:
download_result = utils.download( download_result = utils.download(
remote_voicepack.url, archive_file, file_len=remote_voicepack.size remote_voicepack.url, archive_file, file_len=remote_voicepack.size
@ -505,13 +510,14 @@ class UpdateCommand(Command):
) )
except Exception as e: except Exception as e:
progress.finish( progress.finish(
f"<error>Couldn't apply update: {e} \n({traceback.format_exc()})</error>" f"<error>Couldn't apply update: {e} \n{traceback.format_exc()}</error>"
) )
return return
progress.finish( progress.finish(
f"<comment>Update applied for language {remote_voicepack.language}.</comment>" f"<comment>Update applied for language {remote_voicepack.language.name}.</comment>"
) )
self.line("Setting version config... ") self.line("Setting version config... ")
State.game.version_override = None
State.game.set_version_config() State.game.set_version_config()
self.line( self.line(
f"The game has been updated to version: <comment>{State.game.get_version_str()}</comment>" f"The game has been updated to version: <comment>{State.game.get_version_str()}</comment>"
@ -542,7 +548,7 @@ class RepairCommand(Command):
State.game.repair_game() State.game.repair_game()
except Exception as e: except Exception as e:
progress.finish( progress.finish(
f"<error>Repairation failed with following error: {e} \n({traceback.format_exc()})</error>" f"<error>Repairation failed with following error: {e} \n{traceback.format_exc()}</error>"
) )
return return
progress.finish("<comment>Repairation completed.</comment>") progress.finish("<comment>Repairation completed.</comment>")
@ -578,7 +584,7 @@ class UpdateDownloadCommand(Command):
game_info = State.game.get_remote_game(pre_download=pre_download) game_info = State.game.get_remote_game(pre_download=pre_download)
except Exception as e: except Exception as e:
progress.finish( progress.finish(
f"<error>Update checking failed with following error: {e} \n({traceback.format_exc()})</error>" f"<error>Update checking failed with following error: {e} \n{traceback.format_exc()}</error>"
) )
return return
if update_diff is None or isinstance(game_info.major, str | None): if update_diff is None or isinstance(game_info.major, str | None):
@ -602,7 +608,9 @@ class UpdateDownloadCommand(Command):
update_game_url, out_path, file_len=update_diff.game_pkgs[0].size update_game_url, out_path, file_len=update_diff.game_pkgs[0].size
) )
except Exception as e: except Exception as e:
self.line_error(f"<error>Couldn't download update: {e}</error>") self.line_error(
f"<error>Couldn't download update: {e} \n{traceback.format_exc()}</error>"
)
return return
if not download_result: if not download_result:
@ -616,7 +624,12 @@ class UpdateDownloadCommand(Command):
if remote_voicepack.language not in installed_voicepacks: if remote_voicepack.language not in installed_voicepacks:
continue continue
# Voicepack is installed, update it # Voicepack is installed, update it
archive_file = State.game.cache.joinpath(PurePath(remote_voicepack.url).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}'..."
)
try: try:
download_result = utils.download( download_result = utils.download(
remote_voicepack.url, archive_file, file_len=remote_voicepack.size remote_voicepack.url, archive_file, file_len=remote_voicepack.size
@ -652,7 +665,7 @@ class ApplyUpdateArchive(Command):
State.game.apply_update_archive(update_archive, auto_repair=auto_repair) State.game.apply_update_archive(update_archive, auto_repair=auto_repair)
except Exception as e: except Exception as e:
progress.finish( progress.finish(
f"<error>Couldn't apply update: {e} \n({traceback.format_exc()})</error>" f"<error>Couldn't apply update: {e} \n{traceback.format_exc()}</error>"
) )
return return
progress.finish("<comment>Update applied.</comment>") progress.finish("<comment>Update applied.</comment>")

View File

@ -20,13 +20,18 @@ class GamePackage:
self.decompressed_size = decompressed_size self.decompressed_size = decompressed_size
@staticmethod @staticmethod
def from_dict(data: dict) -> "GamePackage": def from_dict(data: list[dict]) -> list["GamePackage"]:
return GamePackage( game_pkgs = []
url=data["url"], for pkg in data:
md5=data["md5"], game_pkgs.append(
size=int(data["size"]), GamePackage(
decompressed_size=int(data["decompressed_size"]), url=pkg["url"],
) md5=pkg["md5"],
size=int(pkg["size"]),
decompressed_size=int(pkg["decompressed_size"]),
)
)
return game_pkgs
class AudioPackage: class AudioPackage:
@ -45,14 +50,19 @@ class AudioPackage:
self.decompressed_size = decompressed_size self.decompressed_size = decompressed_size
@staticmethod @staticmethod
def from_dict(data: dict) -> "AudioPackage": def from_dict(data: list[dict]) -> "AudioPackage":
return AudioPackage( audio_pkgs = []
language=VoicePackLanguage.from_remote_str(data["language"]), for pkg in data:
url=data["url"], audio_pkgs.append(
md5=data["md5"], AudioPackage(
size=int(data["size"]), language=VoicePackLanguage.from_remote_str(pkg["language"]),
decompressed_size=int(data["decompressed_size"]), url=pkg["url"],
) md5=pkg["md5"],
size=int(pkg["size"]),
decompressed_size=int(pkg["decompressed_size"]),
)
)
return audio_pkgs
class Major: class Major:
@ -72,8 +82,8 @@ class Major:
def from_dict(data: dict) -> "Major": def from_dict(data: dict) -> "Major":
return Major( return Major(
version=data["version"], version=data["version"],
game_pkgs=[GamePackage(**x) for x in data["game_pkgs"]], game_pkgs=GamePackage.from_dict(data["game_pkgs"]),
audio_pkgs=[AudioPackage(**x) for x in data["audio_pkgs"]], audio_pkgs=AudioPackage.from_dict(data["audio_pkgs"]),
res_list_url=data["res_list_url"], res_list_url=data["res_list_url"],
) )

View File

@ -45,10 +45,14 @@ def apply_update_archive(
pass pass
# Think for me a better name for this variable # Think for me a better name for this variable
txtfiles = archive.read(["deletefiles.txt", "hdifffiles.txt"]) txtfiles = archive.read(["deletefiles.txt", "hdifffiles.txt"])
# Reset archive to extract files
archive.reset()
try: try:
# miHoYo loves CRLF # miHoYo loves CRLF
deletebytes = txtfiles["deletefiles.txt"].read() deletebytes = txtfiles["deletefiles.txt"].read()
if deletebytes is bytes: if deletebytes is not str:
# Typing
deletebytes: bytes
deletebytes = deletebytes.decode() deletebytes = deletebytes.decode()
deletefiles = deletebytes.split("\r\n") deletefiles = deletebytes.split("\r\n")
except IOError: except IOError:
@ -69,7 +73,9 @@ def apply_update_archive(
# Read hdifffiles.txt to get the files to patch # Read hdifffiles.txt to get the files to patch
hdifffiles = [] hdifffiles = []
hdiffbytes = txtfiles["hdifffiles.txt"].read() hdiffbytes = txtfiles["hdifffiles.txt"].read()
if hdiffbytes is bytes: if hdiffbytes is not str:
# Typing
hdiffbytes: bytes
hdiffbytes = hdiffbytes.decode() hdiffbytes = hdiffbytes.decode()
for x in hdiffbytes.split("\r\n"): for x in hdiffbytes.split("\r\n"):
try: try:
@ -78,13 +84,9 @@ def apply_update_archive(
pass pass
# Patch function # Patch function
def extract_and_patch(file, patch_file): def 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
# Spaghetti code :(, fuck my eyes. # Spaghetti code :(, fuck my eyes.
archive.extract(game.temppath, [patch_file])
file = file.rename(file.with_suffix(file.suffix + ".bak")) file = file.rename(file.with_suffix(file.suffix + ".bak"))
try: try:
_hdiff.patch_file(file, file.with_suffix(""), patchpath) _hdiff.patch_file(file, file.with_suffix(""), patchpath)
@ -106,18 +108,9 @@ def apply_update_archive(
# Remove old file, since we don't need it anymore. # Remove old file, since we don't need it anymore.
file.unlink() file.unlink()
def extract_or_repair(file: str):
# Extract file
try:
archive.extract(game.path, [file])
except Exception as e:
# Repair file
if not auto_repair:
raise e
game.repair_file(game.path.joinpath(file))
# Multi-threaded patching # Multi-threaded patching
patch_jobs = [] patch_jobs = []
patch_files = []
for file_str in hdifffiles: for file_str in hdifffiles:
file = game.path.joinpath(file_str) file = game.path.joinpath(file_str)
if not file.exists(): if not file.exists():
@ -126,8 +119,13 @@ def apply_update_archive(
patch_file: str = file_str + ".hdiff" patch_file: str = file_str + ".hdiff"
# Remove hdiff files from files list to extract # Remove hdiff files from files list to extract
files.remove(patch_file) files.remove(patch_file)
patch_jobs.append([extract_and_patch, [file, patch_file]]) # Add file to extract list
patch_files.append(patch_file)
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
# Create new ThreadPoolExecutor for patching # Create new ThreadPoolExecutor for patching
patch_executor = concurrent.futures.ThreadPoolExecutor() patch_executor = concurrent.futures.ThreadPoolExecutor()
for job in patch_jobs: for job in patch_jobs:
@ -135,15 +133,7 @@ def apply_update_archive(
patch_executor.shutdown(wait=True) patch_executor.shutdown(wait=True)
# Extract files from archive after we have filtered out the patch files # Extract files from archive after we have filtered out the patch files
# Using ProcessPoolExecutor instead of archive.extractall() because archive.extract(game.path, files)
# archive.extractall() can crash with large archives, and it doesn't
# handle broken files.
# ProcessPoolExecutor is faster than ThreadPoolExecutor, and it shouldn't
# cause any problems here.
extract_executor = concurrent.futures.ProcessPoolExecutor()
for file in files:
extract_executor.submit(extract_or_repair, file)
extract_executor.shutdown(wait=True)
# Close the archive # Close the archive
archive.close() archive.close()

View File

@ -106,7 +106,6 @@ class Game(GameABC):
return False return False
if ( if (
not self._path.joinpath("StarRail.exe").exists() not self._path.joinpath("StarRail.exe").exists()
or not self._path.joinpath("StarRailBase.dll").exists()
or not self._path.joinpath("StarRail_Data").exists() or not self._path.joinpath("StarRail_Data").exists()
): ):
return False return False
@ -166,11 +165,13 @@ class Game(GameABC):
if not cfg_file.exists(): if not cfg_file.exists():
return (0, 0, 0) return (0, 0, 0)
cfg = ConfigFile(cfg_file) cfg = ConfigFile(cfg_file)
if "General" not in cfg.sections(): # Fk u miHoYo
if "general" in cfg.sections():
version_str = cfg.get("general", "game_version", fallback="0.0.0")
elif "General" in cfg.sections():
version_str = cfg.get("General", "game_version", fallback="0.0.0")
else:
return (0, 0, 0) return (0, 0, 0)
if "game_version" not in cfg["General"]:
return (0, 0, 0)
version_str = cfg["General"]["game_version"]
if version_str.count(".") != 2: if version_str.count(".") != 2:
return (0, 0, 0) return (0, 0, 0)
try: try:
@ -196,10 +197,14 @@ class Game(GameABC):
{ {
"General": { "General": {
"channel": 1, "channel": 1,
"cps": "hoyoverse_PC", "cps": "hyp_hoyoverse",
"game_version": self.get_version_str(), "game_version": self.get_version_str(),
"sub_channel": 1, "sub_channel": 1,
"plugin_2_version": "0.0.1", "plugin_2_version": "0.0.1",
"uapc": {
"hkrpg_global": {"uapc": "f5c7c6262812_"},
"hyp": {"uapc": ""},
}, # Honestly what's this?
} }
} }
) )
@ -294,19 +299,22 @@ class Game(GameABC):
if not self.is_installed(): if not self.is_installed():
raise GameNotInstalledError("Game is not installed.") raise GameNotInstalledError("Game is not installed.")
voicepacks = [] voicepacks = []
blacklisted_words = ["SFX"]
for child in ( for child in (
self.data_folder() self.data_folder()
.joinpath("Persistent/Audio/AudioPackage/Windows/") .joinpath("Persistent/Audio/AudioPackage/Windows/")
.iterdir() .iterdir()
): ):
if child.is_dir(): if child.resolve().is_dir() and child.name not in blacklisted_words:
try: try:
voicepacks.append(VoicePackLanguage[child.name]) voicepacks.append(VoicePackLanguage[child.name])
except ValueError: except ValueError:
pass pass
return voicepacks return voicepacks
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. Gets the current game information from remote.
@ -352,9 +360,7 @@ class Game(GameABC):
def _repair_file(self, file: PathLike, game: resource.Main) -> None: def _repair_file(self, file: PathLike, game: resource.Main) -> None:
# .replace("\\", "/") is needed because Windows uses backslashes :) # .replace("\\", "/") is needed because Windows uses backslashes :)
relative_file = file.relative_to(self._path) relative_file = file.relative_to(self._path)
url = ( url = game.major.res_list_url + "/" + str(relative_file).replace("\\", "/")
game.major.res_list_url + "/" + str(relative_file).replace("\\", "/")
)
# Backup the file # Backup the file
if file.exists(): if file.exists():
backup_file = file.with_suffix(file.suffix + ".bak") backup_file = file.with_suffix(file.suffix + ".bak")