Support downloading game & voicepacks and install, bump to 1.1.0

In CLI too, also optimized code & added test for voiceover functions.
This commit is contained in:
tretrauit 2022-02-18 11:11:55 +07:00
parent 07ba17b576
commit bb37e4554d
Signed by: tretrauit
GPG Key ID: 862760FF1903319E
5 changed files with 194 additions and 26 deletions

View File

@ -9,7 +9,7 @@ README = (HERE / "README.md").read_text()
setup(
name='worthless',
version='1.0.0',
version='1.1.0',
packages=['worthless'],
url='https://git.froggi.es/tretrauit/worthless-launcher',
license='MIT License',

View File

@ -6,7 +6,7 @@ game_launcher = worthless.Launcher(overseas=False)
game_installer = worthless.Installer(overseas=False)
class LauncherOverseasTest(unittest.TestCase):
class LauncherCNTest(unittest.TestCase):
def test_get_version_info(self):
version_info = asyncio.run(game_launcher.get_resource_info())
print("get_resource_info test.")

View File

@ -35,13 +35,26 @@ class LauncherOverseasTest(unittest.TestCase):
self.assertIsInstance(bg_url, str)
self.assertTrue(bg_url)
def test_get_installer_diff(self):
def test_get_installer_game_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)
def test_get_installer_voiceover_diff_one(self):
game_diff = asyncio.run(game_installer.get_voiceover_diff_archive("en-us", "2.4.0"))
print("get_voiceover_diff_archive test one (en-us)")
print("get_voiceover_diff_archive: ", game_diff)
print("raw: ", game_diff.raw)
self.assertIsInstance(game_diff, installer.Voicepack)
def test_get_installer_voiceover_diff_two(self):
game_diff = asyncio.run(game_installer.get_voiceover_diff_archive("en-us", "2.4.0"))
print("get_voiceover_diff_archive test two (English(US))")
print("get_voiceover_diff_archive: ", game_diff)
print("raw: ", game_diff.raw)
self.assertIsInstance(game_diff, installer.Voicepack)
if __name__ == '__main__':
unittest.main()

View File

@ -1,6 +1,8 @@
#!/usr/bin/python3
import argparse
import asyncio
import appdirs
from pathlib import Path
from worthless.launcher import Launcher
@ -17,8 +19,11 @@ class UI:
self._installer = Installer(gamedir, data_dir=tempdir)
self._patcher = Patcher(gamedir)
@staticmethod
def _ask(question):
def _ask(self, question):
if self._noconfirm:
# Fake dialog
print(question + " (y/n): y")
return True
answer = ""
while answer.lower() not in ['y', 'n']:
if answer != "":
@ -76,16 +81,76 @@ class UI:
self._install_from_archive(filepath)
print("Game installed successfully.")
def install_game(self):
# TODO
raise NotImplementedError("Install game is not implemented.")
def install_game(self, forced: bool = False):
res_info = asyncio.run(self._launcher.get_resource_info())
print("Latest game version: {}".format(res_info.game.latest.version))
if not self._ask("Do you want to install the game?"):
print("Aborting game installation process.")
return
print("Downloading full game (This will take a long time)...")
asyncio.run(self._installer.download_full_game())
print("Installing game...")
self._install_from_archive(self._installer.temp_path.joinpath(res_info.game.latest.name))
def install_voiceover(self, languages: str):
res_info = asyncio.run(self._launcher.get_resource_info())
print("Latest game version: {}".format(res_info.game.latest.version))
for lng in languages.split(" "):
for vo in res_info.game.latest.voice_packs:
if not self._installer.voiceover_lang_translate(lng) == vo.language:
continue
if not self._ask("Do you want to install this voiceover pack? ({})".format(lng)):
print("Aborting voiceover installation process.")
return
print("Downloading voiceover pack (This will take a long time)...")
asyncio.run(self._installer.download_full_voiceover(lng))
print("Installing voiceover pack...")
self._apply_voiceover_from_archive(self._installer.temp_path.joinpath(res_info.game.latest.name))
break
def update_game(self):
print("Checking for current game version...")
# Call check_game_version()
print("Updating game...")
# Call update_game(fromver)
raise NotImplementedError("Update game is not implemented.")
game_ver = self._installer.get_game_version()
if not game_ver:
self.install_game()
return
print("Current game installation detected. ({})".format(game_ver))
diff_archive = asyncio.run(self._installer.get_game_diff_archive())
res_info = asyncio.run(self._launcher.get_resource_info())
if not diff_archive:
print("No game updates available.")
return
print("Latest game version: {}".format(res_info.game.latest.version))
if not self._ask("Do you want to update the game?"):
print("Aborting game update process.")
return
print("Downloading game update (This will take a long time)...")
asyncio.run(self._installer.download_game_update())
print("Installing game update...")
self.install_from_file(self._installer.temp_path.joinpath(res_info.game.latest.name))
def update_voiceover(self, languages: str):
game_ver = self._installer.get_game_version()
if not game_ver:
self.install_voiceover(languages)
return
print("Current game installation detected. ({})".format(game_ver))
for lng in languages.split(" "):
diff_archive = asyncio.run(self._installer.get_voiceover_diff_archive(lng))
# res_info = asyncio.run(self._launcher.get_resource_info())
if not diff_archive:
print("No voiceover updates available for {}.".format(lng))
continue
if not self._ask("Do you want to update this voiceover? ({})".format(lng)):
print("Aborting this voiceover language update process.")
continue
print("Downloading voiceover update (This may takes some time)...")
asyncio.run(self._installer.download_voiceover_update(lng))
print("Installing voiceover update for {}...".format(lng))
self._apply_voiceover_from_archive(self._installer.temp_path.joinpath(diff_archive.name))
def update_game_voiceover(self, languages: str):
self.update_game()
self.update_voiceover(languages)
def interactive_ui(self):
raise NotImplementedError()
@ -109,9 +174,9 @@ def main():
else update from archive)")
parser.add_argument("-Sp", "--patch", action="store_true",
help="Patch the game (if not already patched, else do nothing)")
parser.add_argument("-Sy", "--update", action="store_true",
parser.add_argument("-Sy", "--update", action="store", type=str,
help="Update the game and specified voiceover pack only (or install if not found)")
parser.add_argument("-Sv", "--update-voiceover", action="store_true",
parser.add_argument("-Sv", "--update-voiceover", action="store", type=str,
help="Update the voiceover pack only (or install if not found)")
parser.add_argument("-Syu", "--update-all", action="store_true",
help="Update the game and all installed voiceover packs (or install if not found)")
@ -125,7 +190,7 @@ def main():
args = parser.parse_args()
interactive_mode = not args.install and not args.install_from_file and not args.patch and not args.update and not \
args.remove and not args.remove_patch and not args.remove_voiceover and not args.get_game_version and not \
args.install_voiceover_from_file
args.install_voiceover_from_file and not args.update_voiceover
if args.temporary_dir:
args.temporary_dir.mkdir(parents=True, exist_ok=True)
@ -150,7 +215,10 @@ def main():
ui.install_game()
if args.update:
ui.update_game()
ui.update_game_voiceover(args.update)
if args.update_voiceover:
ui.update_voiceover(args.update_voiceover)
if args.install_from_file:
ui.install_from_file(args.install_from_file)

View File

@ -1,13 +1,14 @@
import re
import shutil
import aiohttp
import appdirs
import zipfile
import warnings
import json
from pathlib import Path
from configparser import ConfigParser
from aiopath import AsyncPath
from worthless import constants
from worthless.launcher import Launcher
@ -75,11 +76,12 @@ class Installer:
self._gamedir = gamedir
if not data_dir:
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
self._temp_path = Path(self._appdirs.user_cache_dir).joinpath("Installer")
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("Installer")
else:
if not isinstance(data_dir, Path):
data_dir = Path(data_dir)
self._temp_path = data_dir.joinpath("Temp/Installer/")
self.temp_path = data_dir.joinpath("Temp/Installer/")
self.temp_path.mkdir(parents=True, exist_ok=True)
config_file = self._gamedir.joinpath("config.ini")
self._config_file = config_file.resolve()
self._version = None
@ -87,6 +89,33 @@ class Installer:
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
self._version = self.get_game_version()
async def _download_file(self, file_url: str, file_name: str, file_len: int = None):
"""
Download file name to temporary directory,
:param file_url:
:param file_name:
:return:
"""
params = {}
file_path = AsyncPath(self.temp_path).joinpath(file_name)
if file_path.exists():
async with file_path.open("rb") as f:
cur_len = len(await f.read())
params |= {
"Range": f"bytes={cur_len}-{file_len if file_len else ''}"
}
else:
await file_path.touch()
async with aiohttp.ClientSession() as session:
rsp = await session.get(file_url, params=params, timeout=None)
rsp.raise_for_status()
while True:
chunk = await rsp.content.read(8192)
if not chunk:
break
async with file_path.open("ab") as f:
await f.write(chunk)
def get_game_archive_version(self, game_archive: str | Path):
if not game_archive.exists():
raise FileNotFoundError(f"Game archive {game_archive} not found")
@ -110,6 +139,7 @@ class Installer:
return "zh-cn"
case "Korean":
return "ko-kr"
return lang
@staticmethod
def get_voiceover_archive_language(voiceover_archive: str | Path):
@ -174,20 +204,53 @@ class Installer:
archive.extractall(self._gamedir, members=files)
archive.close()
# Update game version on local variable.
self._version = self.get_game_version()
async def download_game_update(self):
if self._version is None:
raise ValueError("Game version not found, use install_game to install the game.")
async def download_full_game(self):
archive = await self._launcher.get_resource_info()
if archive is None:
raise RuntimeError("Failed to fetch game resource info.")
if self._version == archive.game.latest.version:
raise ValueError("Game is already up to date.")
await self._download_file(archive.game.latest.path, archive.game.latest.name, archive.game.latest.size)
async def download_full_voiceover(self, language: str):
archive = await self._launcher.get_resource_info()
if archive is None:
raise RuntimeError("Failed to fetch game resource info.")
translated_lang = self.voiceover_lang_translate(language)
for vo in archive.game.latest.voice_packs:
if vo.language == translated_lang:
await self._download_file(vo.path, vo.name, vo.size)
async def download_game_update(self, from_version: str = None):
if not from_version:
self._version = from_version
if not from_version:
raise ValueError("Game version not found")
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()
diff_archive = await self.get_game_diff_archive(from_version)
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.")
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
async def download_voiceover_update(self, language: str, from_version: str = None):
if not from_version:
self._version = from_version
if not from_version:
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.")
diff_archive = await self.get_voiceover_diff_archive(language, from_version)
if diff_archive is None:
raise ValueError("Voiceover diff archive is not available for this version, please reinstall.")
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
def uninstall_game(self):
shutil.rmtree(self._gamedir)
@ -211,6 +274,30 @@ class Installer:
archive.extractall(self._gamedir)
archive.close()
async def get_voiceover_diff_archive(self, lang: str, from_version: str = None):
"""Gets a diff archive from `from_version` to the latest one
If from_version is not specified, it will be taken from the game version.
"""
if not from_version:
if self._version:
from_version = self._version
else:
from_version = self._version = self.get_game_version()
if not from_version:
raise ValueError("No game version found")
game_resource = await self._launcher.get_resource_info()
if not game_resource:
raise ValueError("Could not fetch game resource")
translated_lang = self.voiceover_lang_translate(lang)
for v in game_resource.game.diffs:
if v.version != from_version:
continue
for vo in v.voice_packs:
if vo.language != translated_lang:
continue
return vo
async def get_game_diff_archive(self, from_version: str = None):
"""Gets a diff archive from `from_version` to the latest one