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(
|
setup(
|
||||||
name='worthless',
|
name='worthless',
|
||||||
version='1.2.9-4',
|
version='1.3.0',
|
||||||
packages=['worthless', 'worthless.classes', 'worthless.classes.launcher', 'worthless.classes.installer'],
|
packages=['worthless', 'worthless.classes', 'worthless.classes.launcher', 'worthless.classes.installer'],
|
||||||
url='https://git.froggi.es/tretrauit/worthless-launcher',
|
url='https://git.froggi.es/tretrauit/worthless-launcher',
|
||||||
license='MIT License',
|
license='MIT License',
|
||||||
|
@ -2,3 +2,4 @@ from worthless import launcher, installer
|
|||||||
|
|
||||||
Launcher = launcher.Launcher
|
Launcher = launcher.Launcher
|
||||||
Installer = installer.Installer
|
Installer = installer.Installer
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -59,11 +59,13 @@ class UI:
|
|||||||
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 (this may takes some time)...")
|
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):
|
def _install_from_archive(self, filepath, force_reinstall):
|
||||||
print("Installing game from archive (this may takes some time)...")
|
print("Installing game from archive (this may takes some time)...")
|
||||||
self._installer.install_game(filepath, force_reinstall)
|
self._installer.install_game(filepath, force_reinstall)
|
||||||
|
self._installer.set_version_config()
|
||||||
|
|
||||||
def _apply_voiceover_from_archive(self, filepath):
|
def _apply_voiceover_from_archive(self, filepath):
|
||||||
print("Applying voiceover from archive (this may takes some time)...")
|
print("Applying voiceover from archive (this may takes some time)...")
|
||||||
@ -311,6 +313,9 @@ def main():
|
|||||||
if args.install:
|
if args.install:
|
||||||
ui.install_game()
|
ui.install_game()
|
||||||
|
|
||||||
|
if args.update_all:
|
||||||
|
raise NotImplementedError() # TODO
|
||||||
|
|
||||||
if args.update:
|
if args.update:
|
||||||
ui.update_game_voiceover(args.update)
|
ui.update_game_voiceover(args.update)
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import platform
|
import platform
|
||||||
@ -11,9 +12,11 @@ from configparser import ConfigParser
|
|||||||
from aiopath import AsyncPath
|
from aiopath import AsyncPath
|
||||||
from worthless import constants
|
from worthless import constants
|
||||||
from worthless.launcher import Launcher
|
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,
|
Download file name to temporary directory,
|
||||||
:param file_url:
|
:param file_url:
|
||||||
@ -45,7 +48,7 @@ async def _download_file(file_url: str, file_name: str, file_path: Path | str, f
|
|||||||
class HDiffPatch:
|
class HDiffPatch:
|
||||||
def __init__(self, git_url=None, data_dir=None):
|
def __init__(self, git_url=None, data_dir=None):
|
||||||
if not git_url:
|
if not git_url:
|
||||||
repo_url = constants.HDIFFPATCH_GIT_URL
|
git_url = constants.HDIFFPATCH_GIT_URL
|
||||||
self._git_url = git_url
|
self._git_url = git_url
|
||||||
if not data_dir:
|
if not data_dir:
|
||||||
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
||||||
@ -82,6 +85,8 @@ class HDiffPatch:
|
|||||||
def _get_hdiffpatch_exec(self, exec_name):
|
def _get_hdiffpatch_exec(self, exec_name):
|
||||||
if shutil.which(exec_name):
|
if shutil.which(exec_name):
|
||||||
return exec_name
|
return exec_name
|
||||||
|
if not self.data_path.exists():
|
||||||
|
return None
|
||||||
if not any(self.data_path.iterdir()):
|
if not any(self.data_path.iterdir()):
|
||||||
return None
|
return None
|
||||||
platform_arch_path = self.data_path.joinpath(self._get_platform_arch())
|
platform_arch_path = self.data_path.joinpath(self._get_platform_arch())
|
||||||
@ -92,6 +97,12 @@ class HDiffPatch:
|
|||||||
def get_hpatchz_executable(self):
|
def get_hpatchz_executable(self):
|
||||||
return self._get_hdiffpatch_exec("hpatchz")
|
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):
|
def get_hdiffz_executable(self):
|
||||||
return self._get_hdiffpatch_exec("hdiffz")
|
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),
|
rsp = await session.get("https://api.github.com/repos/{}/{}/releases/latest".format(owner, repo),
|
||||||
params={"Headers": "Accept: application/vnd.github.v3+json"})
|
params={"Headers": "Accept: application/vnd.github.v3+json"})
|
||||||
rsp.raise_for_status()
|
rsp.raise_for_status()
|
||||||
for asset in await rsp.json()["assets"]:
|
for asset in (await rsp.json())["assets"]:
|
||||||
if asset["name"].endswith(".zip") and not "linux" in asset["name"] and not "windows" in asset["name"] \
|
if asset["name"].endswith(".zip") and "linux" not in asset["name"] and "windows" not in asset["name"] \
|
||||||
and not "macos" in asset["name"] and not "android" in asset["name"]:
|
and "macos" not in asset["name"] and "android" not in asset["name"]:
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
async def get_latest_release_url(self):
|
async def get_latest_release_url(self):
|
||||||
@ -201,11 +212,11 @@ class Installer:
|
|||||||
config_file = self._gamedir.joinpath("config.ini")
|
config_file = self._gamedir.joinpath("config.ini")
|
||||||
self._config_file = config_file.resolve()
|
self._config_file = config_file.resolve()
|
||||||
self._download_chunk = 8192
|
self._download_chunk = 8192
|
||||||
self._version = None
|
|
||||||
self._overseas = overseas
|
self._overseas = overseas
|
||||||
|
self._version = self.get_game_version()
|
||||||
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
|
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
|
||||||
self._hdiffpatch = HDiffPatch(data_dir=data_dir)
|
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):
|
def set_download_chunk(self, chunk: int):
|
||||||
self._download_chunk = chunk
|
self._download_chunk = chunk
|
||||||
@ -280,7 +291,7 @@ class Installer:
|
|||||||
archive.extractall(self._gamedir)
|
archive.extractall(self._gamedir)
|
||||||
archive.close()
|
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():
|
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(game_archive, str):
|
if isinstance(game_archive, str):
|
||||||
@ -288,6 +299,10 @@ 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')
|
||||||
|
|
||||||
|
if not self._hdiffpatch.get_hpatchz_executable():
|
||||||
|
await self._hdiffpatch.download_latest_release()
|
||||||
|
|
||||||
files = archive.namelist()
|
files = archive.namelist()
|
||||||
# Don't extract these files (they're useless and if the game isn't patched then it'll
|
# Don't extract these files (they're useless and if the game isn't patched then it'll
|
||||||
# raise 31-4xxx error ingame)
|
# raise 31-4xxx error ingame)
|
||||||
@ -297,6 +312,35 @@ class Installer:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
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")
|
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)
|
||||||
@ -310,6 +354,12 @@ class Installer:
|
|||||||
# Update game version on local variable.
|
# Update game version on local variable.
|
||||||
self._version = self.get_game_version()
|
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):
|
async def download_full_game(self, overwrite: bool = False):
|
||||||
if self._version and not overwrite:
|
if self._version and not overwrite:
|
||||||
raise ValueError("Game already exists")
|
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