Added voiceover language info & archive type and install_game function

Available through Installer.get_voiceover_archive_type and Installer.get_voiceover_archive_language
Also some other optimizations including not extracting unneeded files from diff archive, deprecate _read_version_from_config function, and added install_game, uninstall_game, voiceover_lang_translate, get_installed_voiceovers
This commit is contained in:
2022-02-17 22:02:08 +07:00
parent b1a9223c19
commit ef24ad43ca
4 changed files with 160 additions and 57 deletions

View File

@ -1,6 +1,10 @@
import re
import shutil
import appdirs
import zipfile
import warnings
import json
from pathlib import Path
from configparser import ConfigParser
@ -8,26 +12,36 @@ from worthless import constants
from worthless.launcher import Launcher
def read_version_from_game_file(globalgamemanagers: Path | bytes):
if isinstance(globalgamemanagers, Path):
with globalgamemanagers.open("rb") as f:
data = f.read().decode("ascii", errors="ignore")
else:
data = globalgamemanagers.decode("ascii", errors="ignore")
result = re.search(r"([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+", data)
if not result:
raise ValueError("Could not find version in game file")
return result.group(1)
class Installer:
def _read_version_from_config(self):
warnings.warn("This function is not reliable as upgrading game version from worthless\
doesn't write the config.", DeprecationWarning)
if not self._config_file.exists():
raise FileNotFoundError(f"Config file {self._config_file} not found")
cfg = ConfigParser()
cfg.read(str(self._config_file))
return cfg.get("General", "game_version")
@staticmethod
def read_version_from_game_file(globalgamemanagers: Path | bytes):
"""
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
Uses `An Anime Game Launcher` method to read the version:
https://gitlab.com/KRypt0n_/an-anime-game-launcher/-/blob/main/src/ts/Game.ts#L26
:return: Game version (ex 1.0.0)
"""
if isinstance(globalgamemanagers, Path):
with globalgamemanagers.open("rb") as f:
data = f.read().decode("ascii", errors="ignore")
else:
data = globalgamemanagers.decode("ascii", errors="ignore")
result = re.search(r"([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+", data)
if not result:
raise ValueError("Could not find version in game file")
return result.group(1)
def get_game_data_name(self):
if self._overseas:
return "GenshinImpact_Data/"
@ -37,15 +51,23 @@ class Installer:
def get_game_data_path(self):
return self._gamedir.joinpath(self.get_game_data_name())
# https://gitlab.com/KRypt0n_/an-anime-game-launcher/-/blob/main/src/ts/Game.ts#L26
def get_game_version(self):
if self._config_file.exists():
return self._read_version_from_config()
else:
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
if not globalgamemanagers.exists():
return
return read_version_from_game_file(globalgamemanagers)
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
if not globalgamemanagers.exists():
return
return self.read_version_from_game_file(globalgamemanagers)
def get_installed_voiceovers(self):
"""
Returns a list of installed voiceovers.
:return: List of installed voiceovers
"""
voiceovers = []
for file in self.get_game_data_path().joinpath("StreamingAssets/Audio/GeneratedSoundBanks/Windows").iterdir():
if file.is_dir():
voiceovers.append(file.name)
return voiceovers
def __init__(self, gamedir: str | Path = Path.cwd(), overseas: bool = True, data_dir: str | Path = None):
if isinstance(gamedir, str):
@ -65,17 +87,60 @@ class Installer:
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
self._version = self.get_game_version()
def get_archive_version(self, game_archive: str | Path):
def get_game_archive_version(self, game_archive: str | Path):
if not game_archive.exists():
raise FileNotFoundError(f"Game archive {game_archive} not found")
archive = zipfile.ZipFile(game_archive, 'r')
return read_version_from_game_file(archive.read(self.get_game_data_name() + "globalgamemanagers"))
return self.read_version_from_game_file(archive.read(self.get_game_data_name() + "globalgamemanagers"))
@staticmethod
def voiceover_lang_translate(lang: str):
"""
Translates the voiceover language to the language code used by the game.
:param lang: Language to translate
:return: Language code
"""
match lang:
case "English(US)":
return "en-us"
case "Japanese":
return "ja-jp"
case "Chinese":
return "zh-cn"
case "Korean":
return "ko-kr"
@staticmethod
def get_voiceover_archive_language(voiceover_archive: str | Path):
if isinstance(voiceover_archive, str):
voiceover_archive = Path(voiceover_archive).resolve()
if not voiceover_archive.exists():
raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found")
archive = zipfile.ZipFile(voiceover_archive, 'r')
archive_path = zipfile.Path(archive)
for file in archive_path.iterdir():
if file.name.endswith("_pkg_version"):
return file.name.split("_")[1]
def get_voiceover_archive_type(self, voiceover_archive: str | Path):
vo_lang = self.get_voiceover_archive_language(voiceover_archive)
archive = zipfile.ZipFile(voiceover_archive, 'r')
archive_path = zipfile.Path(archive)
files = archive.read("Audio_{}_pkg_version".format(vo_lang)).decode().split("\n")
for file in files:
if file.strip() and not archive_path.joinpath(json.loads(file)["remoteName"]).exists():
return False
return True
def apply_voiceover(self, voiceover_archive: str | Path):
# Since Voiceover packages are unclear about diff package or full package
# we will try to extract the voiceover package and apply it to the game
# making this function universal for both cases
if not self.get_game_data_path().exists():
raise FileNotFoundError(f"Game not found in {self._gamedir}")
if isinstance(voiceover_archive, str):
game_archive = Path(voiceover_archive).resolve()
voiceover_archive = Path(voiceover_archive).resolve()
if not voiceover_archive.exists():
raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found")
archive = zipfile.ZipFile(voiceover_archive, 'r')
@ -90,6 +155,15 @@ class Installer:
if not game_archive.exists():
raise FileNotFoundError(f"Update archive {game_archive} not found")
archive = zipfile.ZipFile(game_archive, 'r')
files = archive.namelist()
# Don't extract these files (they're useless and if the game isn't patched then it'll
# raise 31-4xxx error ingame)
for file in ["deletefiles.txt", "hdifffiles.txt"]:
try:
files.remove(file)
except ValueError:
pass
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
for file in deletefiles:
current_game_file = self._gamedir.joinpath(file)
@ -97,28 +171,45 @@ class Installer:
continue
if current_game_file.is_file():
current_game_file.unlink(missing_ok=True)
archive.extractall(self._gamedir)
archive.close()
self._gamedir.joinpath("deletefiles.txt").unlink(missing_ok=True)
self._gamedir.joinpath("hdifffiles.txt").unlink(missing_ok=True)
async def install_game(self, force_reinstall: bool = False):
archive.extractall(self._gamedir, members=files)
archive.close()
async def download_game_update(self):
if self._version is None:
raise ValueError("Game version not found, use install_game to install the game.")
version_info = await self._launcher.get_resource_info()
if version_info is None:
raise RuntimeError("Failed to fetch game resource info.")
if self._version == version_info.game.latest.version:
raise ValueError("Game is already up to date.")
diff_archive = self.get_game_diff_archive()
if diff_archive is None:
raise ValueError("Game diff archive is not available for this version, please reinstall.")
# TODO: Download the diff archive
raise NotImplementedError("Downloading game diff archive is not implemented yet.")
def uninstall_game(self):
shutil.rmtree(self._gamedir)
def install_game(self, game_archive: str | Path, force_reinstall: bool = False):
"""Installs the game to the current directory
If `from_version` is not specified, it will be taken from the game version.
If `to_version` is not specified, it will be taken from the game version.
If `force_reinstall` is True, the game will be uninstalled then reinstalled.
"""
raise NotImplementedError("Not implemented yet")
# if not force:
# if self._temp_path.exists():
# raise FileExistsError(f"Directory {self._temp_path} already exists")
# self._temp_path.mkdir(parents=True, exist_ok=True)
# self._launcher.set_temp_path(self._temp_path)
# await self._launcher.download_game_diff_archive(from_version, to_version)
# await self._launcher.extract_game_diff_archive()
# await self._launcher.install_game_diff_archive()
# self._launcher.set_temp_path(None)
# self._temp_path.rmdir()
if self.get_game_data_path().exists():
if not force_reinstall:
raise ValueError(f"Game is already installed in {self._gamedir}")
self.uninstall_game()
self._gamedir.mkdir(parents=True, exist_ok=True)
if isinstance(game_archive, str):
game_archive = Path(game_archive).resolve()
if not game_archive.exists():
raise FileNotFoundError(f"Install archive {game_archive} not found")
archive = zipfile.ZipFile(game_archive, 'r')
archive.extractall(self._gamedir)
archive.close()
async def get_game_diff_archive(self, from_version: str = None):
"""Gets a diff archive from `from_version` to the latest one