Compare commits
38 Commits
Author | SHA1 | Date |
---|---|---|
tretrauit | df00e6b679 | |
tretrauit | 07f6ce6317 | |
tretrauit | fba2063bce | |
tretrauit | 78df1d242a | |
tretrauit | f1eb1fe2c6 | |
tretrauit | f549166e2e | |
tretrauit | efc7ff2be9 | |
tretrauit | 5e46b23752 | |
tretrauit | 973ae2a5e6 | |
tretrauit | aa4fe4d5ed | |
tretrauit | 836c843b2e | |
tretrauit | 0af4c4f2df | |
tretrauit | a6600cf573 | |
tretrauit | 305021d8b7 | |
tretrauit | 2d5c75109a | |
tretrauit | 45006ef4b5 | |
tretrauit | 5a6f8e39af | |
tretrauit | d99760422d | |
tretrauit | 3c6d44d983 | |
tretrauit | 1de9c42c1f | |
Nguyễn Thế Hưng | cc6f3996af | |
tretrauit | 1957f8265b | |
tretrauit | 820bc70e9d | |
tretrauit | 089b799a5f | |
tretrauit | f46902879a | |
tretrauit | 95dccf7241 | |
tretrauit | 178f513e52 | |
tretrauit | 98898827e9 | |
tretrauit | 95f21e6d2a | |
tretrauit | e55d6cafd6 | |
tretrauit | 727b7e9b44 | |
tretrauit | 0b51be1649 | |
tretrauit | abdf19bc16 | |
tretrauit | e0f3fadc9d | |
tretrauit | cd2aabea60 | |
tretrauit | 1830081cb0 | |
tretrauit | f54c2f432b | |
tretrauit | e3045f5150 |
|
@ -2,4 +2,8 @@
|
|||
|
||||
A worthless CLI launcher written in Python.
|
||||
|
||||
Check out its website at https://tretrauit.gitlab.io/worthless-launcher for more information.
|
||||
> For a nice GUI launcher you should check out [An Anime Game Launcher](https://github.com/an-anime-team/an-anime-game-launcher)
|
||||
|
||||
Check out its website at https://tretrauit.gitlab.io/worthless-launcher for more information.
|
||||
|
||||
> The current branch will enter maintenance mode, for the latest development please check `refactor` branch
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
aiohttp==3.8.1
|
||||
aiohttp==3.8.3
|
||||
appdirs~=1.4.4
|
||||
aiopath~=0.6.10
|
7
setup.py
7
setup.py
|
@ -1,4 +1,5 @@
|
|||
import pathlib
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
# The directory containing this file
|
||||
|
@ -9,12 +10,12 @@ README = (HERE / "README.md").read_text()
|
|||
|
||||
setup(
|
||||
name='worthless',
|
||||
version='2.2.1',
|
||||
version='2.2.22',
|
||||
packages=['worthless', 'worthless.classes', 'worthless.classes.launcher', 'worthless.classes.installer'],
|
||||
url='https://git.froggi.es/tretrauit/worthless-launcher',
|
||||
url='https://git.tretrauit.me/tretrauit/worthless-launcher',
|
||||
license='MIT License',
|
||||
author='tretrauit',
|
||||
author_email='tretrauit@cachyos.org',
|
||||
author_email='tretrauit@gmail.org',
|
||||
description='A worthless CLI launcher written in Python.',
|
||||
long_description=README,
|
||||
long_description_content_type="text/markdown",
|
||||
|
|
|
@ -3,3 +3,5 @@ from worthless import launcher, installer
|
|||
Launcher = launcher.Launcher
|
||||
Installer = installer.Installer
|
||||
|
||||
|
||||
__version__ = "2.2.22"
|
||||
|
|
|
@ -15,7 +15,10 @@ class Latest:
|
|||
self.raw = raw
|
||||
|
||||
def get_name(self):
|
||||
return self.path.split("/")[-1]
|
||||
name = self.path.split("/")[-1]
|
||||
if name == "":
|
||||
name = self.segments[0]["path"].split("/")[-1][:-4]
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data):
|
||||
|
|
|
@ -12,7 +12,7 @@ import worthless.constants as constants
|
|||
|
||||
|
||||
class UI:
|
||||
def __init__(self, gamedir: str, noconfirm: bool, tempdir: str | Path = None, pre_download=False) -> None:
|
||||
def __init__(self, gamedir: str, noconfirm: bool, tempdir: str | Path = None, pre_download: bool = False, download_chunk: int = 8192) -> None:
|
||||
self._vo_version = None
|
||||
self._noconfirm = noconfirm
|
||||
self._gamedir = gamedir
|
||||
|
@ -22,6 +22,7 @@ class UI:
|
|||
self._pre_download = pre_download
|
||||
if self._pre_download:
|
||||
print("Pre-download is enabled, use at your own risk!")
|
||||
self._installer.set_download_chunk(download_chunk)
|
||||
|
||||
def _ask(self, question):
|
||||
if self._noconfirm:
|
||||
|
@ -155,7 +156,7 @@ class UI:
|
|||
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
||||
continue
|
||||
print("Downloading voiceover update pack for {} (This will take a long time)...".format(lng))
|
||||
await self._installer.download_voiceover_update(lng, pre_download=self._pre_download)
|
||||
await self._installer.download_voiceover_update(lng, from_version=self._vo_version, pre_download=self._pre_download)
|
||||
|
||||
async def install_game(self, forced: bool = False):
|
||||
res_info = await self._launcher.get_resource_info()
|
||||
|
@ -315,11 +316,12 @@ async def main():
|
|||
"detection", type=str, default=None)
|
||||
parser.add_argument("--noconfirm", action="store_true",
|
||||
help="Do not ask any for confirmation. (Ignored in interactive mode)")
|
||||
parser.add_argument("--download-chunk", action="store", help="Chunks to download (default 8192 bytes)", type=int, default=8192)
|
||||
args = parser.parse_args()
|
||||
if args.temporary_dir:
|
||||
args.temporary_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ui = UI(args.dir, args.noconfirm, args.temporary_dir, args.predownload)
|
||||
ui = UI(args.dir, args.noconfirm, args.temporary_dir, args.predownload, args.download_chunk)
|
||||
|
||||
if args.install and args.update:
|
||||
raise ValueError("Cannot specify both --install and --update arguments.")
|
||||
|
@ -336,6 +338,9 @@ async def main():
|
|||
if args.from_ver:
|
||||
ui.override_game_version(args.from_ver)
|
||||
|
||||
if args.from_vo_ver:
|
||||
ui.override_voiceover_version(args.from_vo_ver)
|
||||
|
||||
if args.get_game_version:
|
||||
await ui.get_game_version()
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import platform
|
||||
import aiohttp
|
||||
import zipfile
|
||||
import json
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from configparser import ConfigParser
|
||||
from aiopath import AsyncPath
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
|
||||
from worthless import constants
|
||||
from worthless.launcher import Launcher
|
||||
from worthless.launcherconfig import LauncherConfig
|
||||
|
@ -23,27 +24,40 @@ async def _download_file(file_url: str, file_name: str, file_path: Path | str, f
|
|||
:return:
|
||||
"""
|
||||
headers = {}
|
||||
file_path = AsyncPath(file_path).joinpath(file_name)
|
||||
file_path = Path(file_path).joinpath(file_name)
|
||||
if overwrite:
|
||||
await file_path.unlink(missing_ok=True)
|
||||
if await file_path.exists():
|
||||
cur_len = (await file_path.stat()).st_size
|
||||
file_path.unlink(missing_ok=True)
|
||||
if file_path.exists():
|
||||
cur_len = (file_path.stat()).st_size
|
||||
headers |= {
|
||||
"Range": f"bytes={cur_len}-{file_len if file_len else ''}"
|
||||
}
|
||||
else:
|
||||
await file_path.touch()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
file_path.touch()
|
||||
print(f"Downloading {file_url} to {file_path}...")
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=60*60, sock_read=240)) as session:
|
||||
rsp = await session.get(file_url, headers=headers, timeout=None)
|
||||
if rsp.status == 416:
|
||||
return
|
||||
rsp.raise_for_status()
|
||||
while True:
|
||||
chunk = await rsp.content.read(chunks)
|
||||
await asyncio.sleep(0)
|
||||
if not chunk:
|
||||
break
|
||||
async with file_path.open("ab") as f:
|
||||
await f.write(chunk)
|
||||
with file_path.open("ab") as f:
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def calculate_md5(file_to_calculate):
|
||||
file_to_calculate = Path(file_to_calculate)
|
||||
if not file_to_calculate.exists():
|
||||
return ""
|
||||
with file_to_calculate.open("rb") as f:
|
||||
file_hash = hashlib.md5()
|
||||
while chunk := f.read(1024 * 1024):
|
||||
file_hash.update(chunk)
|
||||
return file_hash.hexdigest()
|
||||
|
||||
|
||||
class HDiffPatch:
|
||||
|
@ -60,23 +74,32 @@ class HDiffPatch:
|
|||
data_dir = Path(data_dir)
|
||||
self.data_path = Path(data_dir).joinpath("Tools/HDiffPatch")
|
||||
self.temp_path = data_dir.joinpath("Temp/HDiffPatch")
|
||||
self.temp_path.mkdir(parents=True, exist_ok=True)
|
||||
Path(self.temp_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@staticmethod
|
||||
def _get_platform_arch():
|
||||
processor = platform.machine()
|
||||
match platform.system():
|
||||
case "Windows":
|
||||
match platform.architecture()[0]:
|
||||
case "32bit":
|
||||
match processor:
|
||||
case "i386":
|
||||
return "windows32"
|
||||
case "64bit":
|
||||
case "x86_64":
|
||||
return "windows64"
|
||||
case "arm":
|
||||
return "windows_arm32"
|
||||
case "arm64":
|
||||
return "windows_arm64"
|
||||
case "Linux":
|
||||
match platform.architecture()[0]:
|
||||
case "32bit":
|
||||
match processor:
|
||||
case "i386":
|
||||
return "linux32"
|
||||
case "64bit":
|
||||
case "x86_64":
|
||||
return "linux64"
|
||||
case "arm":
|
||||
return "linux_arm32"
|
||||
case "arm64":
|
||||
return "linux_arm64"
|
||||
case "Darwin":
|
||||
return "macos"
|
||||
|
||||
|
@ -102,6 +125,7 @@ class HDiffPatch:
|
|||
return self._get_hdiffpatch_exec(hpatchz_name)
|
||||
|
||||
async def patch_file(self, in_file, out_file, patch_file, error=False, wait=False):
|
||||
print("executing hpatchz")
|
||||
hpatchz = self.get_hpatchz_executable()
|
||||
if not hpatchz:
|
||||
raise RuntimeError("hpatchz executable not found")
|
||||
|
@ -125,10 +149,13 @@ 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()
|
||||
archive_processor = self._get_platform_arch()
|
||||
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
|
||||
if not asset["name"].endswith(".zip"):
|
||||
continue
|
||||
if archive_processor not in asset["name"]:
|
||||
continue
|
||||
return asset
|
||||
|
||||
async def get_latest_release_url(self):
|
||||
asset = await self._get_latest_release_info()
|
||||
|
@ -151,22 +178,22 @@ class HDiffPatch:
|
|||
|
||||
|
||||
class Installer:
|
||||
def __init__(self, gamedir: str | Path | AsyncPath = AsyncPath.cwd(),
|
||||
overseas: bool = True, data_dir: str | Path | AsyncPath = None):
|
||||
def __init__(self, gamedir: str | Path = Path.cwd(),
|
||||
overseas: bool = True, data_dir: str | Path = None):
|
||||
if isinstance(gamedir, str | Path):
|
||||
gamedir = AsyncPath(gamedir)
|
||||
gamedir = Path(gamedir)
|
||||
self._gamedir = gamedir
|
||||
if not data_dir:
|
||||
self._appdirs = constants.APPDIRS
|
||||
self.temp_path = AsyncPath(self._appdirs.user_cache_dir).joinpath("Installer")
|
||||
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("Installer")
|
||||
else:
|
||||
if isinstance(data_dir, str | AsyncPath):
|
||||
data_dir = AsyncPath(data_dir)
|
||||
if isinstance(data_dir, str | Path):
|
||||
data_dir = Path(data_dir)
|
||||
self.temp_path = data_dir.joinpath("Temp/Installer/")
|
||||
Path(self.temp_path).mkdir(parents=True, exist_ok=True)
|
||||
config_file = self._gamedir.joinpath("config.ini")
|
||||
self._config_file = config_file
|
||||
self._download_chunk = 8192
|
||||
self._download_chunk = 1024 * 1024
|
||||
self._overseas = overseas
|
||||
self._version = None
|
||||
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
|
||||
|
@ -185,13 +212,13 @@ class Installer:
|
|||
chunks=self._download_chunk)
|
||||
|
||||
async def read_version_from_config(self):
|
||||
if not await self._config_file.exists():
|
||||
if not self._config_file.exists():
|
||||
raise FileNotFoundError(f"Config file {self._config_file} not found")
|
||||
cfg = ConfigParser()
|
||||
await asyncio.to_thread(cfg.read, str(self._config_file))
|
||||
return cfg.get("General", "game_version")
|
||||
|
||||
async def read_version_from_game_file(self, globalgamemanagers: AsyncPath | Path | bytes) -> str:
|
||||
async def read_version_from_game_file(self, globalgamemanagers: Path | Path | bytes) -> str:
|
||||
"""
|
||||
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
|
||||
|
||||
|
@ -200,9 +227,9 @@ class Installer:
|
|||
|
||||
:return: Game version (ex 1.0.0)
|
||||
"""
|
||||
if isinstance(globalgamemanagers, Path | AsyncPath):
|
||||
globalgamemanagers = AsyncPath(globalgamemanagers)
|
||||
data = await globalgamemanagers.read_text("ascii", errors="ignore")
|
||||
if isinstance(globalgamemanagers, Path | Path):
|
||||
globalgamemanagers = Path(globalgamemanagers)
|
||||
data = globalgamemanagers.read_text("ascii", errors="ignore")
|
||||
else:
|
||||
data = globalgamemanagers.decode("ascii", errors="ignore")
|
||||
result = self._game_version_re.search(data)
|
||||
|
@ -242,7 +269,7 @@ class Installer:
|
|||
return lang
|
||||
|
||||
@staticmethod
|
||||
async def get_voiceover_archive_language(voiceover_archive: str | Path | AsyncPath) -> str:
|
||||
async def get_voiceover_archive_language(voiceover_archive: str | Path | Path) -> str:
|
||||
if isinstance(voiceover_archive, str | Path):
|
||||
voiceover_archive = Path(voiceover_archive).resolve()
|
||||
if not voiceover_archive.exists():
|
||||
|
@ -259,7 +286,7 @@ class Installer:
|
|||
:param voiceover_archive:
|
||||
:return: True if this is a full archive, else False.
|
||||
"""
|
||||
vo_lang = Installer.get_voiceover_archive_language(voiceover_archive)
|
||||
vo_lang = await Installer.get_voiceover_archive_language(voiceover_archive)
|
||||
with zipfile.ZipFile(voiceover_archive, 'r') as f:
|
||||
archive_path = zipfile.Path(f)
|
||||
files = (await asyncio.to_thread(f.read, "Audio_{}_pkg_version".format(vo_lang))).decode().split("\n")
|
||||
|
@ -277,7 +304,7 @@ class Installer:
|
|||
else:
|
||||
return "YuanShen_Data/"
|
||||
|
||||
def get_game_data_path(self) -> AsyncPath:
|
||||
def get_game_data_path(self) -> Path:
|
||||
return self._gamedir.joinpath(self.get_game_data_name())
|
||||
|
||||
async def get_game_archive_version(self, game_archive: str | Path):
|
||||
|
@ -291,7 +318,7 @@ class Installer:
|
|||
|
||||
async def get_game_version(self) -> str | None:
|
||||
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
|
||||
if not await globalgamemanagers.exists():
|
||||
if not globalgamemanagers.exists():
|
||||
try:
|
||||
return await self.read_version_from_config()
|
||||
except FileNotFoundError:
|
||||
|
@ -305,26 +332,20 @@ class Installer:
|
|||
:return: List of installed voiceovers
|
||||
"""
|
||||
voiceovers = []
|
||||
async for file in self.get_game_data_path()\
|
||||
.joinpath("StreamingAssets/Audio/GeneratedSoundBanks/Windows").iterdir():
|
||||
if await file.is_dir():
|
||||
for file in self.get_game_data_path()\
|
||||
.joinpath("StreamingAssets/AudioAssets/").iterdir():
|
||||
if file.is_dir():
|
||||
voiceovers.append(file.name)
|
||||
return voiceovers
|
||||
|
||||
async def update_game(self, game_archive: str | Path | AsyncPath):
|
||||
if not await self.get_game_data_path().exists():
|
||||
raise FileNotFoundError(f"Game not found in {self._gamedir}")
|
||||
if isinstance(game_archive, str | Path):
|
||||
game_archive = Path(game_archive).resolve()
|
||||
if not game_archive.exists():
|
||||
raise FileNotFoundError(f"Update archive {game_archive} not found")
|
||||
|
||||
async def _update(self, game_archive: str | Path | Path):
|
||||
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)
|
||||
for file in ["deletefiles.txt", "hdifffiles.txt"]:
|
||||
|
@ -332,53 +353,98 @@ class Installer:
|
|||
files.remove(file)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
deletefiles = archive.read("deletefiles.txt").decode().split("\r\n")
|
||||
for file in deletefiles:
|
||||
current_game_file = Path(self._gamedir).joinpath(file)
|
||||
if current_game_file == Path(self._gamedir):
|
||||
# Don't delete the game folder
|
||||
print("Game folder detected, not deleting:", current_game_file)
|
||||
continue
|
||||
if not current_game_file.relative_to(Path(self._gamedir)):
|
||||
print("Not deleting (not relative to game):", current_game_file)
|
||||
continue
|
||||
print("Deleting", file)
|
||||
current_game_file.unlink(missing_ok=True)
|
||||
except Exception as e:
|
||||
print(f"Error while reading deletefiles.txt: {e}")
|
||||
|
||||
# hdiffpatch implementation
|
||||
hdifffiles = []
|
||||
for x in (await asyncio.to_thread(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 await current_game_file.exists():
|
||||
# Not patching since we don't have the file
|
||||
continue
|
||||
try:
|
||||
hdifffiles = []
|
||||
for x in (await asyncio.to_thread(archive.read, "hdifffiles.txt")).decode().split("\n"):
|
||||
if x:
|
||||
hdifffiles.append(json.loads(x.strip())["remoteName"])
|
||||
patch_jobs = []
|
||||
cur_jobs = []
|
||||
count = 0
|
||||
for file in hdifffiles:
|
||||
current_game_file = self._gamedir.joinpath(file)
|
||||
if not current_game_file.exists():
|
||||
print("File", file, "not found")
|
||||
# Not patching since we don't have the file
|
||||
continue
|
||||
|
||||
patch_file = str(file) + ".hdiff"
|
||||
patch_file = str(file) + ".hdiff"
|
||||
|
||||
async def extract_and_patch(old_file, diff_file):
|
||||
diff_path = self.temp_path.joinpath(diff_file)
|
||||
if await diff_path.is_file():
|
||||
await diff_path.unlink(missing_ok=True)
|
||||
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 = await old_file.rename(old_file.with_suffix(".bak"))
|
||||
proc = await self._hdiffpatch.patch_file(old_file, old_file.with_suffix(old_suffix),
|
||||
patch_path, wait=True)
|
||||
await patch_path.unlink()
|
||||
if proc.returncode != 0:
|
||||
async def extract_and_patch(old_file, diff_file):
|
||||
patch_path = self.temp_path.joinpath(diff_file)
|
||||
patch_path.unlink(missing_ok=True)
|
||||
try:
|
||||
print(diff_file)
|
||||
await asyncio.to_thread(archive.extract, diff_file, self.temp_path)
|
||||
except FileExistsError:
|
||||
print("Failed to extract diff file", diff_file)
|
||||
return
|
||||
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, wait=True)
|
||||
patch_path.unlink()
|
||||
if proc.returncode == 0:
|
||||
old_file.unlink()
|
||||
return
|
||||
# Let the game download the file.
|
||||
await old_file.rename(old_file.with_suffix(old_suffix))
|
||||
return
|
||||
await old_file.unlink()
|
||||
print("Failed to patch {}, reverting and let the in-game updater do the job...".format(
|
||||
old_file.with_suffix(old_suffix))
|
||||
)
|
||||
try:
|
||||
old_file.rename(old_file.with_suffix(old_suffix))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
files.remove(patch_file)
|
||||
patch_jobs.append(extract_and_patch(current_game_file, patch_file))
|
||||
files.remove(patch_file)
|
||||
# Limit to 8 process running so it doesn't hang the PC.
|
||||
if count == 7:
|
||||
print("add job")
|
||||
patch_jobs.append(cur_jobs)
|
||||
cur_jobs = []
|
||||
count = 0
|
||||
cur_jobs.append(extract_and_patch(current_game_file, patch_file))
|
||||
count += 1
|
||||
|
||||
await asyncio.gather(*patch_jobs)
|
||||
# The last list may have count < 7 and the above code will not add them
|
||||
patch_jobs.append(cur_jobs)
|
||||
for jobs in patch_jobs:
|
||||
print("exec jobs", jobs)
|
||||
await asyncio.gather(*jobs)
|
||||
|
||||
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
|
||||
for file in deletefiles:
|
||||
current_game_file = Path(self._gamedir.joinpath(file))
|
||||
if not current_game_file.exists():
|
||||
continue
|
||||
if current_game_file.is_file():
|
||||
current_game_file.unlink(missing_ok=True)
|
||||
except Exception as e:
|
||||
print(f"Error while reading hdifffiles.txt: {e}")
|
||||
|
||||
await asyncio.to_thread(archive.extractall, self._gamedir, members=files)
|
||||
archive.close()
|
||||
|
||||
async def update_game(self, game_archive: str | Path | Path):
|
||||
if not self.get_game_data_path().exists():
|
||||
raise FileNotFoundError(f"Game not found in {self._gamedir}")
|
||||
if isinstance(game_archive, str | Path):
|
||||
game_archive = Path(game_archive).resolve()
|
||||
if not game_archive.exists():
|
||||
raise FileNotFoundError(f"Update archive {game_archive} not found")
|
||||
|
||||
await self._update(game_archive=game_archive)
|
||||
# Update game version on local variable.
|
||||
self._version = await self.get_game_version()
|
||||
self.set_version_config()
|
||||
|
@ -391,8 +457,27 @@ class Installer:
|
|||
|
||||
async def download_full_game(self, pre_download=False):
|
||||
game = await self._get_game(pre_download)
|
||||
archive_name = game.latest.path.split("/")[-1]
|
||||
await self._download_file(game.latest.path, archive_name, game.latest.size)
|
||||
if not game.latest.path == "":
|
||||
archive_name = game.latest.path.split("/")[-1]
|
||||
if calculate_md5(self.temp_path.joinpath(archive_name)) != game.latest.md5:
|
||||
raise RuntimeError("mismatch md5 for downloaded game archive")
|
||||
return
|
||||
# Segment download
|
||||
base_archive = None
|
||||
for i, segment in enumerate(game.latest.segments):
|
||||
archive_name = segment["path"].split("/")[-1]
|
||||
if i == 0:
|
||||
base_archive = archive_name = Path(archive_name).stem # Remove .001
|
||||
if self.temp_path.joinpath(archive_name + ".downloaded").exists():
|
||||
continue
|
||||
await self._download_file(segment["path"], archive_name)
|
||||
if i != 0:
|
||||
with open(self.temp_path.joinpath(base_archive), 'ab') as f:
|
||||
with open(self.temp_path.joinpath(archive_name), 'rb') as f2:
|
||||
f.write(f2.read())
|
||||
self.temp_path.joinpath(archive_name).unlink()
|
||||
self.temp_path.joinpath(archive_name + ".downloaded").touch()
|
||||
|
||||
|
||||
async def download_full_voiceover(self, language: str, pre_download=False):
|
||||
game = await self._get_game(pre_download)
|
||||
|
@ -404,8 +489,8 @@ class Installer:
|
|||
async def uninstall_game(self):
|
||||
await asyncio.to_thread(shutil.rmtree, self._gamedir, ignore_errors=True)
|
||||
|
||||
async def _extract_game_file(self, archive: str | Path | AsyncPath):
|
||||
if isinstance(archive, str | AsyncPath):
|
||||
async def _extract_game_file(self, archive: str | Path | Path):
|
||||
if isinstance(archive, str | Path):
|
||||
archive = Path(archive).resolve()
|
||||
if not archive.exists():
|
||||
raise FileNotFoundError(f"'{archive}' not found")
|
||||
|
@ -416,21 +501,24 @@ class Installer:
|
|||
# Since Voiceover packages are unclear about diff package or full package
|
||||
# we will try to extract the voiceover package and apply it to the game
|
||||
# making this function universal for both cases
|
||||
if not await self.get_game_data_path().exists():
|
||||
if not self.get_game_data_path().exists():
|
||||
raise FileNotFoundError(f"Game not found in {self._gamedir}")
|
||||
await self._extract_game_file(voiceover_archive)
|
||||
if isinstance(voiceover_archive, str | Path):
|
||||
voiceover_archive = Path(voiceover_archive).resolve()
|
||||
await self._update(voiceover_archive)
|
||||
# await self._extract_game_file(voiceover_archive)
|
||||
|
||||
async def install_game(self, game_archive: str | Path | AsyncPath, force_reinstall: bool = False):
|
||||
async def install_game(self, game_archive: str | Path | Path, force_reinstall: bool = False):
|
||||
"""Installs the game to the current directory
|
||||
|
||||
If `force_reinstall` is True, the game will be uninstalled then reinstalled.
|
||||
"""
|
||||
if await self.get_game_data_path().exists():
|
||||
if self.get_game_data_path().exists():
|
||||
if not force_reinstall:
|
||||
raise ValueError(f"Game is already installed in {self._gamedir}")
|
||||
await self.uninstall_game()
|
||||
|
||||
await self._gamedir.mkdir(parents=True, exist_ok=True)
|
||||
self._gamedir.mkdir(parents=True, exist_ok=True)
|
||||
await self._extract_game_file(game_archive)
|
||||
self._version = await self.get_game_version()
|
||||
self.set_version_config()
|
||||
|
@ -484,7 +572,7 @@ class Installer:
|
|||
game = await self._get_game(pre_download=pre_download)
|
||||
translated_lang = self.voiceover_lang_translate(lang)
|
||||
for v in game.diffs:
|
||||
if v.version != from_version:
|
||||
if v.version.strip() != from_version.strip():
|
||||
continue
|
||||
for vo in v.voice_packs:
|
||||
if vo.language == translated_lang:
|
||||
|
@ -502,21 +590,12 @@ class Installer:
|
|||
if v.version == from_version:
|
||||
return v
|
||||
|
||||
async def verify_from_pkg_version(self, pkg_version: AsyncPath, ignore_mismatch=False):
|
||||
contents = await pkg_version.read_text()
|
||||
|
||||
async def calculate_md5(file_to_calculate):
|
||||
file_to_calculate = AsyncPath(file_to_calculate)
|
||||
if not await file_to_calculate.exists():
|
||||
return ""
|
||||
async with file_to_calculate.open("rb") as f:
|
||||
file_hash = hashlib.md5()
|
||||
while chunk := await f.read(self._download_chunk):
|
||||
file_hash.update(chunk)
|
||||
return file_hash.hexdigest()
|
||||
async def verify_from_pkg_version(self, pkg_version: Path, ignore_mismatch=False):
|
||||
contents = pkg_version.read_text()
|
||||
|
||||
async def verify_file(file_to_verify, md5):
|
||||
file_md5 = await calculate_md5(file_to_verify)
|
||||
print("Verifying file:", file_to_verify)
|
||||
file_md5 = await asyncio.to_thread(calculate_md5, file_to_verify)
|
||||
if file_md5 == md5:
|
||||
return None
|
||||
if ignore_mismatch:
|
||||
|
@ -524,13 +603,23 @@ class Installer:
|
|||
raise ValueError(f"MD5 does not match for {file_to_verify}, expected md5: {md5}, actual md5: {file_md5}")
|
||||
|
||||
verify_jobs = []
|
||||
cur_jobs = []
|
||||
count = 0
|
||||
for content in contents.split("\r\n"):
|
||||
if not content.strip():
|
||||
continue
|
||||
if count >= 7:
|
||||
verify_jobs.append(cur_jobs)
|
||||
cur_jobs = []
|
||||
count = 0
|
||||
info = json.loads(content)
|
||||
verify_jobs.append(verify_file(self._gamedir.joinpath(info["remoteName"]), info["md5"]))
|
||||
cur_jobs.append(verify_file(self._gamedir.joinpath(info["remoteName"]), info["md5"]))
|
||||
count += 1
|
||||
|
||||
verify_result = await asyncio.gather(*verify_jobs)
|
||||
verify_jobs.append(cur_jobs)
|
||||
verify_result = []
|
||||
for jobs in verify_jobs:
|
||||
verify_result.extend(await asyncio.gather(*jobs))
|
||||
failed_files = []
|
||||
for file in verify_result:
|
||||
if file is not None:
|
||||
|
@ -538,7 +627,7 @@ class Installer:
|
|||
|
||||
return None if not failed_files else failed_files
|
||||
|
||||
async def verify_game(self, pkg_version: str | Path | AsyncPath = None, ignore_mismatch=False):
|
||||
async def verify_game(self, pkg_version: str | Path | Path = None, ignore_mismatch=False):
|
||||
if pkg_version is None:
|
||||
pkg_version = self._gamedir.joinpath("pkg_version")
|
||||
return await self.verify_from_pkg_version(pkg_version, ignore_mismatch)
|
||||
|
|
|
@ -29,7 +29,7 @@ except ImportError:
|
|||
|
||||
class Patcher:
|
||||
def __init__(self, gamedir: Path | AsyncPath | str = AsyncPath.cwd(), data_dir: str | Path | AsyncPath = None,
|
||||
patch_url: str = None, overseas=True, patch_provider="y0soro"):
|
||||
patch_url: str = None, overseas=True, patch_provider="Krock"):
|
||||
if isinstance(gamedir, str | Path):
|
||||
gamedir = AsyncPath(gamedir)
|
||||
self._gamedir = gamedir
|
||||
|
@ -237,12 +237,19 @@ class Patcher:
|
|||
disable_files = [
|
||||
self._installer.get_game_data_name() + "upload_crash.exe",
|
||||
self._installer.get_game_data_name() + "Plugins/crashreport.exe",
|
||||
self._installer.get_game_data_name() + "blueReporter.exe",
|
||||
]
|
||||
for file in disable_files:
|
||||
file_path = Path(self._gamedir.joinpath(file)).resolve()
|
||||
if file_path.exists():
|
||||
await AsyncPath(file_path).rename(str(file_path) + ".bak")
|
||||
|
||||
# Delete old Telemetry.dll on Linux (cAsE sEnsItIvE)
|
||||
if platform.system() == "Linux":
|
||||
telemetry_path = Path(self._installer.get_game_data_name()).joinpath("Plugins/Telemetry.dll")
|
||||
if telemetry_path.exists() and Path(self._installer.get_game_data_name()).joinpath("Plugins/telemetry.dll").exists():
|
||||
await telemetry_path.unlink()
|
||||
|
||||
patch_jobs.append(disable_crashreporters())
|
||||
await asyncio.gather(*patch_jobs)
|
||||
|
||||
|
@ -290,6 +297,7 @@ class Patcher:
|
|||
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",
|
||||
self._installer.get_game_data_name() + "blueReporter.exe",
|
||||
]
|
||||
revert_job = []
|
||||
for file in revert_files:
|
||||
|
|
Loading…
Reference in New Issue