Add support for hdiffpatch files

Game now apply update properly (hopefully)

Signed-off-by: tretrauit <tretrauit@gmail.com>
This commit is contained in:
tretrauit 2022-03-31 02:09:33 +07:00
parent c7918f8a20
commit fd00e8b51d
Signed by: tretrauit
GPG Key ID: 862760FF1903319E
6 changed files with 116 additions and 10 deletions

View File

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

View File

@ -2,3 +2,4 @@ from worthless import launcher, installer
Launcher = launcher.Launcher
Installer = installer.Installer

View File

@ -0,0 +1 @@

View File

@ -59,11 +59,13 @@ class UI:
print("Reverting patches if patched...")
self._patcher.revert_patch(True)
print("Updating game from archive (this may takes some time)...")
self._installer.update_game(filepath)
asyncio.run(self._installer.update_game(filepath))
self._installer.set_version_config()
def _install_from_archive(self, filepath, force_reinstall):
print("Installing game from archive (this may takes some time)...")
self._installer.install_game(filepath, force_reinstall)
self._installer.set_version_config()
def _apply_voiceover_from_archive(self, filepath):
print("Applying voiceover from archive (this may takes some time)...")
@ -311,6 +313,9 @@ def main():
if args.install:
ui.install_game()
if args.update_all:
raise NotImplementedError() # TODO
if args.update:
ui.update_game_voiceover(args.update)

View File

@ -1,3 +1,4 @@
import asyncio
import re
import shutil
import platform
@ -11,9 +12,11 @@ from configparser import ConfigParser
from aiopath import AsyncPath
from worthless import constants
from worthless.launcher import Launcher
from worthless.launcherconfig import LauncherConfig
async def _download_file(file_url: str, file_name: str, file_path: Path | str, file_len: int = None, overwrite=False, chunks=8192):
async def _download_file(file_url: str, file_name: str, file_path: Path | str, file_len: int = None, overwrite=False,
chunks=8192):
"""
Download file name to temporary directory,
:param file_url:
@ -45,7 +48,7 @@ async def _download_file(file_url: str, file_name: str, file_path: Path | str, f
class HDiffPatch:
def __init__(self, git_url=None, data_dir=None):
if not git_url:
repo_url = constants.HDIFFPATCH_GIT_URL
git_url = constants.HDIFFPATCH_GIT_URL
self._git_url = git_url
if not data_dir:
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
@ -82,6 +85,8 @@ class HDiffPatch:
def _get_hdiffpatch_exec(self, exec_name):
if shutil.which(exec_name):
return exec_name
if not self.data_path.exists():
return None
if not any(self.data_path.iterdir()):
return None
platform_arch_path = self.data_path.joinpath(self._get_platform_arch())
@ -92,6 +97,12 @@ class HDiffPatch:
def get_hpatchz_executable(self):
return self._get_hdiffpatch_exec("hpatchz")
async def patch_file(self, in_file, out_file, patch_file):
hpatchz = self.get_hpatchz_executable()
if not hpatchz:
raise RuntimeError("hpatchz executable not found")
return await asyncio.create_subprocess_exec(hpatchz, "-f", in_file, patch_file, out_file)
def get_hdiffz_executable(self):
return self._get_hdiffpatch_exec("hdiffz")
@ -103,9 +114,9 @@ class HDiffPatch:
rsp = await session.get("https://api.github.com/repos/{}/{}/releases/latest".format(owner, repo),
params={"Headers": "Accept: application/vnd.github.v3+json"})
rsp.raise_for_status()
for asset in await rsp.json()["assets"]:
if asset["name"].endswith(".zip") and not "linux" in asset["name"] and not "windows" in asset["name"] \
and not "macos" in asset["name"] and not "android" in asset["name"]:
for asset in (await rsp.json())["assets"]:
if asset["name"].endswith(".zip") and "linux" not in asset["name"] and "windows" not in asset["name"] \
and "macos" not in asset["name"] and "android" not in asset["name"]:
return asset
async def get_latest_release_url(self):
@ -201,11 +212,11 @@ class Installer:
config_file = self._gamedir.joinpath("config.ini")
self._config_file = config_file.resolve()
self._download_chunk = 8192
self._version = None
self._overseas = overseas
self._version = self.get_game_version()
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
self._hdiffpatch = HDiffPatch(data_dir=data_dir)
self._version = self.get_game_version()
self._config = LauncherConfig(self._config_file, self._version)
def set_download_chunk(self, chunk: int):
self._download_chunk = chunk
@ -280,7 +291,7 @@ class Installer:
archive.extractall(self._gamedir)
archive.close()
def update_game(self, game_archive: str | Path):
async def update_game(self, game_archive: str | Path):
if not self.get_game_data_path().exists():
raise FileNotFoundError(f"Game not found in {self._gamedir}")
if isinstance(game_archive, str):
@ -288,6 +299,10 @@ class Installer:
if not game_archive.exists():
raise FileNotFoundError(f"Update archive {game_archive} not found")
archive = zipfile.ZipFile(game_archive, 'r')
if not self._hdiffpatch.get_hpatchz_executable():
await self._hdiffpatch.download_latest_release()
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)
@ -297,6 +312,35 @@ class Installer:
except ValueError:
pass
# hdiffpatch implementation
hdifffiles = []
for x in archive.read("hdifffiles.txt").decode().split("\n"):
if x:
hdifffiles.append(json.loads(x)["remoteName"])
patch_jobs = []
for file in hdifffiles:
current_game_file = self._gamedir.joinpath(file)
if not current_game_file.exists():
# Not patching since we don't have the file
continue
patch_file = str(file) + ".hdiff"
async def extract_and_patch(old_file, diff_file):
await asyncio.to_thread(archive.extract, diff_file, self.temp_path)
patch_path = self.temp_path.joinpath(diff_file)
old_suffix = old_file.suffix
old_file = old_file.rename(old_file.with_suffix(".bak"))
proc = await self._hdiffpatch.patch_file(old_file, old_file.with_suffix(old_suffix),
patch_path)
await proc.wait()
patch_path.unlink()
old_file.unlink()
files.remove(patch_file)
patch_jobs.append(extract_and_patch(current_game_file, patch_file))
await asyncio.gather(*patch_jobs)
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
for file in deletefiles:
current_game_file = self._gamedir.joinpath(file)
@ -310,6 +354,12 @@ class Installer:
# Update game version on local variable.
self._version = self.get_game_version()
def set_version_config(self, version: str = None):
if not version:
version = self._version
self._config.set_game_version(version)
self._config.save()
async def download_full_game(self, overwrite: bool = False):
if self._version and not overwrite:
raise ValueError("Game already exists")

View File

@ -0,0 +1,49 @@
from configparser import ConfigParser
from pathlib import Path
class LauncherConfig:
"""
Provides config.ini for official launcher compatibility
"""
@staticmethod
def create_config(game_version, overseas=True):
"""
Creates config.ini
"""
sub_channel = "0" if overseas else "1"
config = ConfigParser()
config.add_section("General")
config.set("General", "channel", "1")
config.set("General", "cps", "mihoyo")
config.set("General", "game_version", game_version)
config.set("General", "sdk_version", "")
config.set("General", "sub_channel", sub_channel)
return config
def __init__(self, config_path, game_version=None, overseas=True):
if isinstance(config_path, str):
self.config_path = Path(config_path)
if not game_version:
game_version = "0.0.0"
self.config_path = config_path
self.config = ConfigParser()
if self.config_path.exists():
self.config.read(self.config_path)
else:
self.config = self.create_config(game_version, overseas)
def set_game_version(self, game_version):
self.config.set("General", "game_version", game_version)
def set_overseas(self, overseas=True):
sub_channel = "0" if overseas else "1"
self.config.set("General", "sub_channel", sub_channel)
def save(self):
"""
Saves config.ini
"""
with self.config_path.open("w") as config_file:
self.config.write(config_file)