Add support for hdiffpatch files

Game now apply update properly (hopefully)

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

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")