Add support for hdiffpatch files
Game now apply update properly (hopefully) Signed-off-by: tretrauit <tretrauit@gmail.com>
This commit is contained in:
parent
c7918f8a20
commit
fd00e8b51d
2
setup.py
2
setup.py
@ -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',
|
||||
|
@ -2,3 +2,4 @@ from worthless import launcher, installer
|
||||
|
||||
Launcher = launcher.Launcher
|
||||
Installer = installer.Installer
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
|
49
worthless/launcherconfig.py
Normal file
49
worthless/launcherconfig.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user