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:
tretrauit 2022-02-17 22:02:08 +07:00
parent b1a9223c19
commit ef24ad43ca
No known key found for this signature in database
GPG Key ID: 862760FF1903319E
4 changed files with 160 additions and 57 deletions

View File

@ -1,38 +1,47 @@
import unittest import unittest
import asyncio import asyncio
import worthless import worthless
from worthless.classes import launcher from worthless.classes import launcher, installer
client = worthless.Launcher(overseas=False) game_launcher = worthless.Launcher(overseas=False)
game_installer = worthless.Installer(overseas=False)
class LauncherCNTest(unittest.TestCase): class LauncherOverseasTest(unittest.TestCase):
def test_get_version_info(self): def test_get_version_info(self):
version_info = asyncio.run(client.get_version_info()) version_info = asyncio.run(game_launcher.get_resource_info())
print("get_version_info test.") print("get_resource_info test.")
print("get_version_info: ", version_info) print("get_resource_info: ", version_info)
self.assertIsInstance(version_info, dict) print("raw: ", version_info.raw)
self.assertIsInstance(version_info, installer.Resource)
def test_get_launcher_info(self): def test_get_launcher_info(self):
launcher_info = asyncio.run(client.get_launcher_info()) launcher_info = asyncio.run(game_launcher.get_launcher_info())
print("get_launcher_info test.") print("get_launcher_info test.")
print("get_launcher_info: ", launcher_info) print("get_launcher_info: ", launcher_info)
print("raw: ", launcher_info.raw) print("raw: ", launcher_info.raw)
self.assertIsInstance(launcher_info, launcher.Info) self.assertIsInstance(launcher_info, launcher.Info)
def test_get_launcher_full_info(self): def test_get_launcher_full_info(self):
launcher_info = asyncio.run(client.get_launcher_full_info()) launcher_info = asyncio.run(game_launcher.get_launcher_full_info())
print("get_launcher_full_info test.") print("get_launcher_full_info test.")
print("get_launcher_full_info: ", launcher_info) print("get_launcher_full_info: ", launcher_info)
print("raw: ", launcher_info.raw) print("raw: ", launcher_info.raw)
self.assertIsInstance(launcher_info, launcher.Info) self.assertIsInstance(launcher_info, launcher.Info)
def test_get_launcher_background_url(self): def test_get_launcher_background_url(self):
bg_url = asyncio.run(client.get_launcher_background_url()) bg_url = asyncio.run(game_launcher.get_launcher_background_url())
print("get_launcher_background_url test.") print("get_launcher_background_url test.")
print("get_launcher_background_url: ", bg_url) print("get_launcher_background_url: ", bg_url)
self.assertIsInstance(bg_url, str) self.assertIsInstance(bg_url, str)
self.assertTrue(bg_url) self.assertTrue(bg_url)
def test_get_installer_diff(self):
game_diff = asyncio.run(game_installer.get_game_diff_archive("2.4.0"))
print("get_game_diff_archive test.")
print("get_game_diff_archive: ", game_diff)
print("raw: ", game_diff.raw)
self.assertIsInstance(game_diff, installer.Diff)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -32,14 +32,17 @@ class UI:
def _update_from_archive(self, filepath): def _update_from_archive(self, filepath):
print("Reverting patches if patched...") print("Reverting patches if patched...")
self._patcher.revert_patch(True) self._patcher.revert_patch(True)
print("Updating game from archive...") print("Updating game from archive (this may takes some time)...")
self._installer.update_game(filepath) self._installer.update_game(filepath)
def _apply_voiceover_from_archive(self, filepath): def _apply_voiceover_from_archive(self, filepath):
print("Applying voiceover from archive...") print("Applying voiceover from archive (this may takes some time)...")
self._installer.apply_voiceover(filepath) self._installer.apply_voiceover(filepath)
def install_voiceover_from_file(self, filepath): def install_voiceover_from_file(self, filepath):
print("Archive voiceover language: {} ({})".format(
self._installer.get_voiceover_archive_language(filepath),
"Full archive" if self._installer.get_voiceover_archive_type(filepath) else "Update archive"))
if not self._ask("Do you want to apply this voiceover pack? ({})".format(filepath)): if not self._ask("Do you want to apply this voiceover pack? ({})".format(filepath)):
print("Aborting apply process.") print("Aborting apply process.")
return return
@ -55,8 +58,8 @@ class UI:
gamever = self._installer.get_game_version() gamever = self._installer.get_game_version()
if gamever: if gamever:
print("Current game installation detected. ({})".format(self._installer.get_game_version())) print("Current game installation detected. ({})".format(self._installer.get_game_version()))
print("Archive game version: " + self._installer.get_archive_version(filepath)) print("Archive game version: " + self._installer.get_game_archive_version(filepath))
if not self._ask("Do you want to update the game? (from {})".format(filepath)): if not self._ask("Do you want to update the game? ({})".format(filepath)):
print("Aborting update process.") print("Aborting update process.")
return return
self._update_from_archive(filepath) self._update_from_archive(filepath)

View File

@ -1,6 +1,10 @@
import re import re
import shutil
import appdirs import appdirs
import zipfile import zipfile
import warnings
import json
from pathlib import Path from pathlib import Path
from configparser import ConfigParser from configparser import ConfigParser
@ -8,26 +12,36 @@ from worthless import constants
from worthless.launcher import Launcher 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: class Installer:
def _read_version_from_config(self): 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(): if not self._config_file.exists():
raise FileNotFoundError(f"Config file {self._config_file} not found") raise FileNotFoundError(f"Config file {self._config_file} not found")
cfg = ConfigParser() cfg = ConfigParser()
cfg.read(str(self._config_file)) cfg.read(str(self._config_file))
return cfg.get("General", "game_version") 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): def get_game_data_name(self):
if self._overseas: if self._overseas:
return "GenshinImpact_Data/" return "GenshinImpact_Data/"
@ -37,15 +51,23 @@ class Installer:
def get_game_data_path(self): def get_game_data_path(self):
return self._gamedir.joinpath(self.get_game_data_name()) 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): def get_game_version(self):
if self._config_file.exists(): globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
return self._read_version_from_config() if not globalgamemanagers.exists():
else: return
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers") return self.read_version_from_game_file(globalgamemanagers)
if not globalgamemanagers.exists():
return def get_installed_voiceovers(self):
return read_version_from_game_file(globalgamemanagers) """
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): def __init__(self, gamedir: str | Path = Path.cwd(), overseas: bool = True, data_dir: str | Path = None):
if isinstance(gamedir, str): if isinstance(gamedir, str):
@ -65,17 +87,60 @@ class Installer:
self._launcher = Launcher(self._gamedir, overseas=self._overseas) self._launcher = Launcher(self._gamedir, overseas=self._overseas)
self._version = self.get_game_version() 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(): if not game_archive.exists():
raise FileNotFoundError(f"Game archive {game_archive} not found") raise FileNotFoundError(f"Game archive {game_archive} not found")
archive = zipfile.ZipFile(game_archive, 'r') 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): 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(): if not self.get_game_data_path().exists():
raise FileNotFoundError(f"Game not found in {self._gamedir}") raise FileNotFoundError(f"Game not found in {self._gamedir}")
if isinstance(voiceover_archive, str): if isinstance(voiceover_archive, str):
game_archive = Path(voiceover_archive).resolve() voiceover_archive = Path(voiceover_archive).resolve()
if not voiceover_archive.exists(): if not voiceover_archive.exists():
raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found") raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found")
archive = zipfile.ZipFile(voiceover_archive, 'r') archive = zipfile.ZipFile(voiceover_archive, 'r')
@ -90,6 +155,15 @@ class Installer:
if not game_archive.exists(): if not game_archive.exists():
raise FileNotFoundError(f"Update archive {game_archive} not found") raise FileNotFoundError(f"Update archive {game_archive} not found")
archive = zipfile.ZipFile(game_archive, 'r') 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") deletefiles = archive.read("deletefiles.txt").decode().split("\n")
for file in deletefiles: for file in deletefiles:
current_game_file = self._gamedir.joinpath(file) current_game_file = self._gamedir.joinpath(file)
@ -97,28 +171,45 @@ class Installer:
continue continue
if current_game_file.is_file(): if current_game_file.is_file():
current_game_file.unlink(missing_ok=True) 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 """Installs the game to the current directory
If `from_version` is not specified, it will be taken from the game version. If `force_reinstall` is True, the game will be uninstalled then reinstalled.
If `to_version` is not specified, it will be taken from the game version.
""" """
raise NotImplementedError("Not implemented yet") if self.get_game_data_path().exists():
# if not force: if not force_reinstall:
# if self._temp_path.exists(): raise ValueError(f"Game is already installed in {self._gamedir}")
# raise FileExistsError(f"Directory {self._temp_path} already exists") self.uninstall_game()
# self._temp_path.mkdir(parents=True, exist_ok=True)
# self._launcher.set_temp_path(self._temp_path) self._gamedir.mkdir(parents=True, exist_ok=True)
# await self._launcher.download_game_diff_archive(from_version, to_version) if isinstance(game_archive, str):
# await self._launcher.extract_game_diff_archive() game_archive = Path(game_archive).resolve()
# await self._launcher.install_game_diff_archive() if not game_archive.exists():
# self._launcher.set_temp_path(None) raise FileNotFoundError(f"Install archive {game_archive} not found")
# self._temp_path.rmdir() archive = zipfile.ZipFile(game_archive, 'r')
archive.extractall(self._gamedir)
archive.close()
async def get_game_diff_archive(self, from_version: str = None): async def get_game_diff_archive(self, from_version: str = None):
"""Gets a diff archive from `from_version` to the latest one """Gets a diff archive from `from_version` to the latest one

View File

@ -127,8 +127,8 @@ class Patcher:
] ]
for file in revert_files: for file in revert_files:
self._revert_file(file, game_exec, ignore_errors) self._revert_file(file, game_exec, ignore_errors)
self._gamedir.joinpath("launcher.bat").unlink(missing_ok=True) for file in ["launcher.bat", "mhyprot2_running.reg"]:
self._gamedir.joinpath("mhyprot2_running.reg").unlink(missing_ok=True) self._gamedir.joinpath(file).unlink(missing_ok=True)
def get_files(extensions): def get_files(extensions):
all_files = [] all_files = []