refactor: convert all task-intensive functions to async.
chore: rename gui.py to cli.py fix: internal downloader can resume download now. feat: add verify_game, verify_from_pkg_version, clear_cache to installer.py. feat: add clear_cache to patcher.py. fix: linux now check for pkexec before executing it. fix: add get_name to voicepack.py, latest.py, diff.py to get name from path (since the developer didn't set a name to these files in the sdk url) chore: remove deprecation message in read_version_from_config in installer.py misc: use chunk from self._download_chunk instead of being hardcoded to 8192. fix: is_telemetry_blocked will only wait 15s for a connection. chore: move appdirs to constants.py This commit refactor almost all functions to be compatible with asyncio, also restructured CLI to use asyncio.run on main function instead of executing it randomly. Also prioritize the use of asyncio.gather, sometimes making tasks faster
This commit is contained in:
@ -1,11 +1,13 @@
|
||||
import os
|
||||
import platform
|
||||
import tarfile
|
||||
import appdirs
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
from aiopath import AsyncPath
|
||||
|
||||
from worthless import constants
|
||||
from worthless.launcher import Launcher
|
||||
from worthless.installer import Installer
|
||||
@ -26,16 +28,19 @@ except ImportError:
|
||||
|
||||
|
||||
class Patcher:
|
||||
def __init__(self, gamedir=Path.cwd(), data_dir: str | Path = None, patch_url: str = None, overseas=True):
|
||||
def __init__(self, gamedir: Path | AsyncPath | str = AsyncPath.cwd(), data_dir: str | Path | AsyncPath = None,
|
||||
patch_url: str = None, overseas=True):
|
||||
if isinstance(gamedir, str | Path):
|
||||
gamedir = AsyncPath(gamedir)
|
||||
self._gamedir = gamedir
|
||||
self._patch_url = (patch_url if patch_url else constants.PATCH_GIT_URL).replace('http://', 'https://')
|
||||
if not data_dir:
|
||||
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
||||
self._patch_path = Path(self._appdirs.user_data_dir).joinpath("Patch")
|
||||
self._temp_path = Path(self._appdirs.user_cache_dir).joinpath("Patcher")
|
||||
self._appdirs = constants.APPDIRS
|
||||
self._patch_path = AsyncPath(self._appdirs.user_data_dir).joinpath("Patch")
|
||||
self._temp_path = AsyncPath(self._appdirs.user_cache_dir).joinpath("Patcher")
|
||||
else:
|
||||
if not isinstance(data_dir, Path):
|
||||
data_dir = Path(data_dir)
|
||||
if isinstance(data_dir, str | Path):
|
||||
data_dir = AsyncPath(data_dir)
|
||||
self._patch_path = data_dir.joinpath("Patch")
|
||||
self._temp_path = data_dir.joinpath("Temp/Patcher")
|
||||
self._overseas = overseas
|
||||
@ -43,7 +48,7 @@ class Patcher:
|
||||
self._launcher = Launcher(self._gamedir, overseas=overseas)
|
||||
match platform.system():
|
||||
case "Linux":
|
||||
self._linuxutils = linux.LinuxUtils()
|
||||
self._linux = linux.LinuxUtils()
|
||||
|
||||
@staticmethod
|
||||
async def _get(url, **kwargs) -> aiohttp.ClientResponse:
|
||||
@ -76,15 +81,15 @@ class Patcher:
|
||||
else:
|
||||
return await archive.read()
|
||||
|
||||
async def _download_repo(self):
|
||||
if shutil.which("git"):
|
||||
if not self._patch_path.exists() or not self._patch_path.is_dir() \
|
||||
or not self._patch_path.joinpath(".git").exists():
|
||||
async def _download_repo(self, fallback=False):
|
||||
if shutil.which("git") and not fallback:
|
||||
if not await self._patch_path.is_dir() or not await self._patch_path.joinpath(".git").exists():
|
||||
proc = await asyncio.create_subprocess_exec("git", "clone", self._patch_url, str(self._patch_path))
|
||||
await proc.wait()
|
||||
else:
|
||||
proc = await asyncio.create_subprocess_exec("git", "pull", cwd=str(self._patch_path))
|
||||
await proc.wait()
|
||||
await proc.wait()
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError("Cannot download patch repository through git.")
|
||||
else:
|
||||
archive = await self._get_git_archive()
|
||||
if not archive:
|
||||
@ -120,24 +125,18 @@ class Patcher:
|
||||
telemetry_url = constants.TELEMETRY_URL_LIST
|
||||
else:
|
||||
telemetry_url = constants.TELEMETRY_URL_CN_LIST
|
||||
if optional:
|
||||
telemetry_url |= constants.TELEMETRY_OPTIONAL_URL_LIST
|
||||
unblocked_list = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for url in telemetry_url:
|
||||
try:
|
||||
await session.get("https://" + url)
|
||||
except (aiohttp.ClientResponseError, aiohttp.ClientConnectorError):
|
||||
await session.get("https://" + url, timeout=15)
|
||||
except (aiohttp.ClientResponseError, aiohttp.ClientConnectorError, asyncio.exceptions.TimeoutError):
|
||||
continue
|
||||
else:
|
||||
unblocked_list.append(url)
|
||||
if optional:
|
||||
for url in constants.TELEMETRY_OPTIONAL_URL_LIST:
|
||||
try:
|
||||
await session.get("https://" + url)
|
||||
except (aiohttp.ClientResponseError, aiohttp.ClientConnectorError):
|
||||
continue
|
||||
else:
|
||||
unblocked_list.append(url)
|
||||
return None if unblocked_list == [] else unblocked_list
|
||||
return None if not unblocked_list else unblocked_list
|
||||
|
||||
async def block_telemetry(self, optional=False):
|
||||
telemetry = await self.is_telemetry_blocked(optional)
|
||||
@ -148,20 +147,15 @@ class Patcher:
|
||||
telemetry_hosts += "0.0.0.0 " + url + "\n"
|
||||
match platform.system():
|
||||
case "Linux":
|
||||
await self._linuxutils.append_text_to_file(telemetry_hosts, "/etc/hosts")
|
||||
await self._linux.append_text_to_file(telemetry_hosts, "/etc/hosts")
|
||||
return
|
||||
# TODO: Windows and macOS
|
||||
raise NotImplementedError("Platform not implemented.")
|
||||
|
||||
async def _patch_unityplayer_fallback(self):
|
||||
# xdelta3-python doesn't work because it's outdated.
|
||||
if self._overseas:
|
||||
patch = "unityplayer_patch_os.vcdiff"
|
||||
else:
|
||||
patch = "unityplayer_patch_cn.vcdiff"
|
||||
gamever = "".join(self._installer.get_game_version().split("."))
|
||||
async def _patch_unityplayer_fallback(self, patch):
|
||||
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||
unity_path = self._gamedir.joinpath("UnityPlayer.dll")
|
||||
unity_path.rename(self._gamedir.joinpath("UnityPlayer.dll.bak"))
|
||||
await unity_path.rename(self._gamedir.joinpath("UnityPlayer.dll.bak"))
|
||||
proc = await asyncio.create_subprocess_exec("xdelta3", "-d", "-s",
|
||||
str(self._gamedir.joinpath("UnityPlayer.dll.bak")),
|
||||
str(self._patch_path.joinpath(
|
||||
@ -169,16 +163,11 @@ class Patcher:
|
||||
str(self._gamedir.joinpath("UnityPlayer.dll")), cwd=self._gamedir)
|
||||
await proc.wait()
|
||||
|
||||
async def _patch_xlua_fallback(self):
|
||||
# xdelta3-python doesn't work becuase it's outdated.
|
||||
if self._overseas:
|
||||
patch = "unityplayer_patch_os.vcdiff"
|
||||
else:
|
||||
patch = "unityplayer_patch_cn.vcdiff"
|
||||
gamever = "".join(self._installer.get_game_version().split("."))
|
||||
async def _patch_xlua_fallback(self, patch):
|
||||
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||
data_name = self._installer.get_game_data_name()
|
||||
xlua_path = self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))
|
||||
xlua_path.rename(self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name)))
|
||||
await xlua_path.rename(self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name)))
|
||||
proc = await asyncio.create_subprocess_exec("xdelta3", "-d", "-s",
|
||||
str(self._gamedir.joinpath(
|
||||
"{}/Plugins/xlua.dll.bak".format(data_name))),
|
||||
@ -189,12 +178,8 @@ class Patcher:
|
||||
cwd=self._gamedir)
|
||||
await proc.wait()
|
||||
|
||||
def _patch_unityplayer(self):
|
||||
if self._overseas:
|
||||
patch = "unityplayer_patch_os.vcdiff"
|
||||
else:
|
||||
patch = "unityplayer_patch_cn.vcdiff"
|
||||
gamever = "".join(self._installer.get_game_version().split("."))
|
||||
async def _patch_unityplayer(self, patch):
|
||||
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||
unity_path = self._gamedir.joinpath("UnityPlayer.dll")
|
||||
patch_bytes = self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch)).read_bytes()
|
||||
patched_unity_bytes = xdelta3.decode(unity_path.read_bytes(), patch_bytes)
|
||||
@ -202,9 +187,8 @@ class Patcher:
|
||||
with Path(self._gamedir.joinpath("UnityPlayer.dll")).open("wb") as f:
|
||||
f.write(patched_unity_bytes)
|
||||
|
||||
def _patch_xlua(self):
|
||||
patch = "xlua_patch.vcdiff"
|
||||
gamever = "".join(self._installer.get_game_version().split("."))
|
||||
async def _patch_xlua(self, patch):
|
||||
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||
data_name = self._installer.get_game_data_name()
|
||||
xlua_path = self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))
|
||||
patch_bytes = self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch)).read_bytes()
|
||||
@ -213,13 +197,17 @@ class Patcher:
|
||||
with Path(self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))).open("wb") as f:
|
||||
f.write(patched_xlua_bytes)
|
||||
|
||||
def apply_xlua_patch(self, fallback=True):
|
||||
async def apply_xlua_patch(self, fallback=True):
|
||||
if self._overseas:
|
||||
patch = "xlua_patch_os.vcdiff"
|
||||
else:
|
||||
patch = "xlua_patch_cn.vcdiff"
|
||||
if NO_XDELTA3_MODULE or fallback:
|
||||
asyncio.run(self._patch_xlua_fallback())
|
||||
await self._patch_xlua_fallback(patch)
|
||||
return
|
||||
self._patch_xlua()
|
||||
await self._patch_xlua(patch)
|
||||
|
||||
def apply_patch(self, crash_fix=False, fallback=True) -> None:
|
||||
async def apply_patch(self, crash_fix=False, fallback=True) -> None:
|
||||
"""
|
||||
Patch the game (and optionally patch xLua if specified)
|
||||
|
||||
@ -228,25 +216,36 @@ class Patcher:
|
||||
:return: None
|
||||
"""
|
||||
# Patch UnityPlayer.dll
|
||||
if NO_XDELTA3_MODULE or fallback:
|
||||
asyncio.run(self._patch_unityplayer_fallback())
|
||||
# xdelta3-python doesn't work because it's outdated.
|
||||
if self._overseas:
|
||||
patch = "unityplayer_patch_os.vcdiff"
|
||||
else:
|
||||
self._patch_unityplayer()
|
||||
patch = "unityplayer_patch_cn.vcdiff"
|
||||
patch_jobs = []
|
||||
if NO_XDELTA3_MODULE or fallback:
|
||||
patch_jobs.append(self._patch_unityplayer_fallback(patch))
|
||||
else:
|
||||
patch_jobs.append(self._patch_unityplayer(patch))
|
||||
# Patch xLua.dll
|
||||
if crash_fix:
|
||||
self.apply_xlua_patch(fallback=fallback)
|
||||
patch_jobs.append(self.apply_xlua_patch(fallback=fallback))
|
||||
# Disable crash reporters
|
||||
disable_files = [
|
||||
self._installer.get_game_data_name() + "upload_crash.exe",
|
||||
self._installer.get_game_data_name() + "Plugins/crashreport.exe",
|
||||
]
|
||||
for file in disable_files:
|
||||
file_path = Path(file).resolve()
|
||||
if file_path.exists():
|
||||
file_path.rename(str(file_path) + ".bak")
|
||||
|
||||
async def disable_crashreporters():
|
||||
disable_files = [
|
||||
self._installer.get_game_data_name() + "upload_crash.exe",
|
||||
self._installer.get_game_data_name() + "Plugins/crashreport.exe",
|
||||
]
|
||||
for file in disable_files:
|
||||
file_path = Path(file).resolve()
|
||||
if file_path.exists():
|
||||
file_path.rename(str(file_path) + ".bak")
|
||||
|
||||
patch_jobs.append(disable_crashreporters())
|
||||
await asyncio.gather(*patch_jobs)
|
||||
|
||||
@staticmethod
|
||||
def _creation_date(file_path: Path):
|
||||
async def _creation_date(file_path: AsyncPath):
|
||||
"""
|
||||
Try to get the date that a file was created, falling back to when it was
|
||||
last modified if that isn't possible.
|
||||
@ -255,7 +254,7 @@ class Patcher:
|
||||
if platform.system() == 'Windows':
|
||||
return os.path.getctime(file_path)
|
||||
else:
|
||||
stat = file_path.stat()
|
||||
stat = await file_path.stat()
|
||||
try:
|
||||
return stat.st_birthtime
|
||||
except AttributeError:
|
||||
@ -263,11 +262,11 @@ class Patcher:
|
||||
# so we'll settle for when its content was last modified.
|
||||
return stat.st_mtime
|
||||
|
||||
def _revert_file(self, original_file: str, base_file: Path, ignore_error=False):
|
||||
original_path = self._gamedir.joinpath(original_file + ".bak").resolve()
|
||||
target_file = self._gamedir.joinpath(original_file).resolve()
|
||||
if original_path.exists():
|
||||
if abs(self._creation_date(base_file) - self._creation_date(original_path)) > 86400: # 24 hours
|
||||
async def _revert_file(self, original_file: str, base_file: AsyncPath, ignore_error=False):
|
||||
original_path = await self._gamedir.joinpath(original_file + ".bak").resolve()
|
||||
target_file = await self._gamedir.joinpath(original_file).resolve()
|
||||
if await original_path.exists():
|
||||
if abs(await self._creation_date(base_file) - await self._creation_date(original_path)) > 86400: # 24 hours
|
||||
if not ignore_error:
|
||||
raise RuntimeError("{} is not for this game version.".format(original_path.name))
|
||||
original_path.unlink(missing_ok=True)
|
||||
@ -275,30 +274,36 @@ class Patcher:
|
||||
target_file.unlink(missing_ok=True)
|
||||
original_path.rename(target_file)
|
||||
|
||||
def revert_patch(self, ignore_errors=True) -> None:
|
||||
async def revert_patch(self, ignore_errors=True) -> None:
|
||||
"""
|
||||
Revert the patch (and revert the login door crash fix if patched)
|
||||
Revert the patch (and revert the xLua patch if patched)
|
||||
|
||||
:return: None
|
||||
"""
|
||||
game_exec = self._gamedir.joinpath(asyncio.run(self._launcher.get_resource_info()).game.latest.entry)
|
||||
game_exec = self._gamedir.joinpath((await self._launcher.get_resource_info()).game.latest.entry)
|
||||
revert_files = [
|
||||
"UnityPlayer.dll",
|
||||
self._installer.get_game_data_name() + "upload_crash.exe",
|
||||
self._installer.get_game_data_name() + "Plugins/crashreport.exe",
|
||||
self._installer.get_game_data_name() + "Plugins/xlua.dll",
|
||||
]
|
||||
revert_job = []
|
||||
for file in revert_files:
|
||||
self._revert_file(file, game_exec, ignore_errors)
|
||||
revert_job.append(self._revert_file(file, game_exec, ignore_errors))
|
||||
for file in ["launcher.bat", "mhyprot2_running.reg"]:
|
||||
self._gamedir.joinpath(file).unlink(missing_ok=True)
|
||||
revert_job.append(self._gamedir.joinpath(file).unlink(missing_ok=True))
|
||||
|
||||
def get_files(extensions):
|
||||
async def get_files(extensions):
|
||||
all_files = []
|
||||
for ext in extensions:
|
||||
all_files.extend(self._gamedir.glob(ext))
|
||||
all_files.extend(await self._gamedir.glob(ext))
|
||||
return all_files
|
||||
|
||||
files = get_files(('*.dxvk-cache', '*_d3d9.log', '*_d3d11.log', '*_dxgi.log'))
|
||||
files = await get_files(('*.dxvk-cache', '*_d3d9.log', '*_d3d11.log', '*_dxgi.log'))
|
||||
for file in files:
|
||||
file.unlink(missing_ok=True)
|
||||
revert_job.append(file.unlink(missing_ok=True))
|
||||
|
||||
await asyncio.gather(*revert_job)
|
||||
|
||||
async def clear_cache(self):
|
||||
await asyncio.to_thread(shutil.rmtree, self._temp_path, ignore_errors=True)
|
||||
|
||||
Reference in New Issue
Block a user