fix: handle .zip update archive
Also fixes some minor bug
This commit is contained in:
@ -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.
|
Since you should use the specific implementation for the target game instead, these functions may not be correctly documented.
|
||||||
|
|
||||||
|
|||||||
@ -114,7 +114,7 @@ class GameABC(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_update(self):
|
def get_update(self) -> resource.Patch | None:
|
||||||
"""
|
"""
|
||||||
Get the game update
|
Get the game update
|
||||||
"""
|
"""
|
||||||
@ -126,7 +126,9 @@ class GameABC(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
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.
|
Gets the current game information from remote.
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from cleo.helpers import option, argument
|
|||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from platform import system
|
from platform import system
|
||||||
from vollerei.abc.launcher.game import GameABC
|
from vollerei.abc.launcher.game import GameABC
|
||||||
|
from vollerei.common.api import resource
|
||||||
from vollerei.common.enums import GameChannel, VoicePackLanguage
|
from vollerei.common.enums import GameChannel, VoicePackLanguage
|
||||||
from vollerei.cli import utils
|
from vollerei.cli import utils
|
||||||
from vollerei.exceptions.game import GameError
|
from vollerei.exceptions.game import GameError
|
||||||
@ -141,9 +142,7 @@ class VoicepackList(Command):
|
|||||||
|
|
||||||
class VoicepackInstall(Command):
|
class VoicepackInstall(Command):
|
||||||
name = "hsr voicepack install"
|
name = "hsr voicepack install"
|
||||||
description = (
|
description = "Installs the specified installed voicepacks"
|
||||||
"Installs the specified installed voicepacks"
|
|
||||||
)
|
|
||||||
options = default_options + [
|
options = default_options + [
|
||||||
option("pre-download", description="Pre-download the game if available"),
|
option("pre-download", description="Pre-download the game if available"),
|
||||||
]
|
]
|
||||||
@ -196,7 +195,9 @@ class VoicepackInstall(Command):
|
|||||||
self.line(
|
self.line(
|
||||||
f"Downloading install package for language: <comment>{remote_voicepack.language.name}</comment>... "
|
f"Downloading install package for language: <comment>{remote_voicepack.language.name}</comment>... "
|
||||||
)
|
)
|
||||||
archive_file = State.game.cache.joinpath(PurePath(remote_voicepack.url).name)
|
archive_file = State.game.cache.joinpath(
|
||||||
|
PurePath(remote_voicepack.url).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
|
||||||
@ -280,7 +281,7 @@ class VoicepackUpdate(Command):
|
|||||||
progress = utils.ProgressIndicator(self)
|
progress = utils.ProgressIndicator(self)
|
||||||
progress.start("Checking for updates... ")
|
progress.start("Checking for updates... ")
|
||||||
try:
|
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)
|
game_info = State.game.get_remote_game(pre_download=pre_download)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
progress.finish(
|
progress.finish(
|
||||||
@ -295,23 +296,25 @@ class VoicepackUpdate(Command):
|
|||||||
f"The current version is: <comment>{State.game.get_version_str()}</comment>"
|
f"The current version is: <comment>{State.game.get_version_str()}</comment>"
|
||||||
)
|
)
|
||||||
self.line(
|
self.line(
|
||||||
f"The latest version is: <comment>{game_info.latest.version}</comment>"
|
f"The latest version is: <comment>{game_info.major.version}</comment>"
|
||||||
)
|
)
|
||||||
if not self.confirm("Do you want to update the game?"):
|
if not self.confirm("Do you want to update the game?"):
|
||||||
self.line("<error>Update aborted.</error>")
|
self.line("<error>Update aborted.</error>")
|
||||||
return
|
return
|
||||||
# Voicepack update
|
# 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:
|
if remote_voicepack.language not in installed_voicepacks:
|
||||||
continue
|
continue
|
||||||
# Voicepack is installed, update it
|
# Voicepack is installed, update it
|
||||||
self.line(
|
archive_file = State.game.cache.joinpath(
|
||||||
f"Downloading update package for language: <comment>{remote_voicepack.language.name}</comment>... "
|
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:
|
try:
|
||||||
download_result = utils.download(
|
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:
|
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}</error>")
|
||||||
@ -334,10 +337,9 @@ class VoicepackUpdate(Command):
|
|||||||
progress.finish(
|
progress.finish(
|
||||||
f"<comment>Update applied for language {remote_voicepack.language.name}.</comment>"
|
f"<comment>Update applied for language {remote_voicepack.language.name}.</comment>"
|
||||||
)
|
)
|
||||||
|
State.game.version_override = game_info.major.version
|
||||||
set_version_config(self=self)
|
set_version_config(self=self)
|
||||||
self.line(
|
State.game.version_override = None
|
||||||
f"The game has been updated to version: <comment>{State.game.get_version_str()}</comment>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PatchTypeCommand(Command):
|
class PatchTypeCommand(Command):
|
||||||
@ -601,7 +603,7 @@ class InstallCommand(Command):
|
|||||||
progress.finish("<comment>Package applied for the base game.</comment>")
|
progress.finish("<comment>Package applied for the base game.</comment>")
|
||||||
self.line("Setting version config... ")
|
self.line("Setting version config... ")
|
||||||
State.game.version_override = game_info.major.version
|
State.game.version_override = game_info.major.version
|
||||||
set_version_config()
|
set_version_config(self=self)
|
||||||
State.game.version_override = None
|
State.game.version_override = None
|
||||||
self.line(
|
self.line(
|
||||||
f"The game has been installed to version: <comment>{State.game.get_version_str()}</comment>"
|
f"The game has been installed to version: <comment>{State.game.get_version_str()}</comment>"
|
||||||
@ -719,7 +721,7 @@ class UpdateCommand(Command):
|
|||||||
)
|
)
|
||||||
self.line("Setting version config... ")
|
self.line("Setting version config... ")
|
||||||
State.game.version_override = game_info.major.version
|
State.game.version_override = game_info.major.version
|
||||||
set_version_config()
|
set_version_config(self=self)
|
||||||
State.game.version_override = None
|
State.game.version_override = None
|
||||||
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>"
|
||||||
@ -940,7 +942,8 @@ class ApplyUpdateArchive(Command):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
progress.finish("<comment>Update applied.</comment>")
|
progress.finish("<comment>Update applied.</comment>")
|
||||||
set_version_config()
|
set_version_config(self=self)
|
||||||
|
|
||||||
|
|
||||||
# This is the list for HSR commands, we'll add Genshin commands later
|
# This is the list for HSR commands, we'll add Genshin commands later
|
||||||
classes = [
|
classes = [
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import concurrent.futures
|
|||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import py7zr
|
import py7zr
|
||||||
|
import zipfile
|
||||||
from io import IOBase
|
from io import IOBase
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -39,9 +40,30 @@ def apply_update_archive(
|
|||||||
|
|
||||||
# Install HDiffPatch
|
# Install HDiffPatch
|
||||||
_hdiff.hpatchz()
|
_hdiff.hpatchz()
|
||||||
|
|
||||||
# Open archive
|
# Open archive
|
||||||
# archive = zipfile.ZipFile(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")
|
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)
|
# Get files list (we don't want to extract all of them)
|
||||||
files = archive.namelist()
|
files = archive.namelist()
|
||||||
# Don't extract these files (they're useless and if the game isn't patched then
|
# 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:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
# Think for me a better name for this variable
|
# Think for me a better name for this variable
|
||||||
|
txtfiles = None
|
||||||
|
if isinstance(archive, py7zr.SevenZipFile):
|
||||||
txtfiles = archive.read(["deletefiles.txt", "hdifffiles.txt"])
|
txtfiles = archive.read(["deletefiles.txt", "hdifffiles.txt"])
|
||||||
# Reset archive to extract files
|
# Reset archive to extract files
|
||||||
archive.reset()
|
archive.reset()
|
||||||
try:
|
try:
|
||||||
# miHoYo loves CRLF
|
# miHoYo loves CRLF
|
||||||
|
if txtfiles is not None:
|
||||||
deletebytes = txtfiles["deletefiles.txt"].read()
|
deletebytes = txtfiles["deletefiles.txt"].read()
|
||||||
|
else:
|
||||||
|
deletebytes = archive.read("deletefiles.txt")
|
||||||
if deletebytes is not str:
|
if deletebytes is not str:
|
||||||
# Typing
|
# Typing
|
||||||
deletebytes: bytes
|
deletebytes: bytes
|
||||||
deletebytes = deletebytes.decode()
|
deletebytes = deletebytes.decode()
|
||||||
deletefiles = deletebytes.split("\r\n")
|
deletefiles = deletebytes.split("\r\n")
|
||||||
except IOError:
|
except (IOError, KeyError):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
for file_str in deletefiles:
|
for file_str in deletefiles:
|
||||||
@ -80,7 +107,10 @@ def apply_update_archive(
|
|||||||
# hdiffpatch implementation
|
# hdiffpatch implementation
|
||||||
# Read hdifffiles.txt to get the files to patch
|
# Read hdifffiles.txt to get the files to patch
|
||||||
hdifffiles = []
|
hdifffiles = []
|
||||||
|
if txtfiles is not None:
|
||||||
hdiffbytes = txtfiles["hdifffiles.txt"].read()
|
hdiffbytes = txtfiles["hdifffiles.txt"].read()
|
||||||
|
else:
|
||||||
|
hdiffbytes = archive.read("hdifffiles.txt")
|
||||||
if hdiffbytes is not str:
|
if hdiffbytes is not str:
|
||||||
# Typing
|
# Typing
|
||||||
hdiffbytes: bytes
|
hdiffbytes: bytes
|
||||||
@ -132,8 +162,8 @@ def apply_update_archive(
|
|||||||
patch_jobs.append([patch, [file, patch_file]])
|
patch_jobs.append([patch, [file, patch_file]])
|
||||||
|
|
||||||
# Extract patch files to temporary dir
|
# Extract patch files to temporary dir
|
||||||
archive.extract(game.cache, patch_files)
|
extract_files(archive, patch_files, game.cache)
|
||||||
archive.reset() # For the next extraction
|
reset_if_py7zr(archive) # 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:
|
||||||
@ -141,7 +171,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
|
||||||
archive.extract(game.path, files)
|
extract_files(archive, files, game.path)
|
||||||
|
|
||||||
# Close the archive
|
# Close the archive
|
||||||
archive.close()
|
archive.close()
|
||||||
|
|||||||
@ -30,7 +30,7 @@ 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/genshin/")
|
self.cache: Path = cache_path.joinpath("game/zzz/")
|
||||||
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
|
||||||
@ -192,7 +192,7 @@ class Game(GameABC):
|
|||||||
Gets the current installed game version.
|
Gets the current installed game version.
|
||||||
|
|
||||||
Credits to An Anime Team for the code that does the magic:
|
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
|
If the above method fails, it'll fallback to read the config.ini file
|
||||||
for the version, which is not recommended (as described in
|
for the version, which is not recommended (as described in
|
||||||
@ -224,25 +224,18 @@ class Game(GameABC):
|
|||||||
for byte in f.read(10000):
|
for byte in f.read(10000):
|
||||||
match byte:
|
match byte:
|
||||||
case 0:
|
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_bytes = [[], [], []]
|
||||||
version_ptr = 0
|
version_ptr = 0
|
||||||
correct = True
|
correct = True
|
||||||
case 46:
|
case b'.':
|
||||||
version_ptr += 1
|
version_ptr += 1
|
||||||
if version_ptr > 2:
|
if version_ptr > 2:
|
||||||
correct = False
|
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 _:
|
case _:
|
||||||
if correct and byte in b"0123456789":
|
if correct and byte in b"0123456789":
|
||||||
version_bytes[version_ptr].append(byte)
|
version_bytes[version_ptr].append(byte)
|
||||||
@ -276,9 +269,7 @@ class Game(GameABC):
|
|||||||
raise GameNotInstalledError("Game is not installed.")
|
raise GameNotInstalledError("Game is not installed.")
|
||||||
voicepacks = []
|
voicepacks = []
|
||||||
for child in (
|
for child in (
|
||||||
self.data_folder()
|
self.data_folder().joinpath("StreamingAssets/Audio/Windows/Full/").iterdir()
|
||||||
.joinpath("StreamingAssets/Audio/Windows/Full/")
|
|
||||||
.iterdir()
|
|
||||||
):
|
):
|
||||||
if child.resolve().is_dir():
|
if child.resolve().is_dir():
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user