Compare commits

...

38 Commits

Author SHA1 Message Date
tretrauit df00e6b679 chore: update url 2024-04-25 21:46:10 +07:00
tretrauit 07f6ce6317 fix: hdiffpatch download 2024-04-25 21:46:03 +07:00
tretrauit fba2063bce chore: bump to 2.2.21 2023-11-08 16:22:48 +07:00
tretrauit 78df1d242a fix(installer): wrap rename in try/except
So it won't fail I guess, because we failed anyway.
2023-11-08 16:22:30 +07:00
tretrauit f1eb1fe2c6 fix: voicepack detection 2023-06-20 22:35:33 +07:00
tretrauit f549166e2e fix: workaround around merged parts 2023-06-20 22:11:10 +07:00
tretrauit efc7ff2be9 fix: implement segments download
michos don't fucking do breaking changes in the API fuck you.
2023-06-20 18:14:44 +07:00
tretrauit 5e46b23752 chore: delete files first to save space 2023-05-25 13:03:05 +07:00
tretrauit 973ae2a5e6 fix: actually delete files in deletefiles.txt 2023-05-25 13:01:04 +07:00
tretrauit aa4fe4d5ed chore: bump to 2.2.19 2023-05-25 10:42:14 +07:00
tretrauit 836c843b2e fix: str -> Path 2023-05-25 10:41:51 +07:00
tretrauit 0af4c4f2df chore: bump 2023-05-25 10:39:59 +07:00
tretrauit a6600cf573 fix: wrong blueReporter path 2023-05-25 10:39:40 +07:00
tretrauit 305021d8b7 chore: bump to 2.2.17 2023-05-25 10:38:25 +07:00
tretrauit 2d5c75109a fix: add blueReporter to revert list 2023-05-25 10:29:24 +07:00
tretrauit 45006ef4b5 fix: telemetry.dll rename 2023-05-25 10:28:17 +07:00
tretrauit 5a6f8e39af feat: add blueReporter 2023-05-25 10:26:08 +07:00
tretrauit d99760422d fix: voicepack detection & 2.2.16 2023-05-25 10:12:01 +07:00
tretrauit 3c6d44d983 fix: --from-vo-ver doesn't set vo version 2023-03-01 20:22:04 +07:00
tretrauit 1de9c42c1f misc: remove comment 2023-03-01 17:33:48 +07:00
Nguyễn Thế Hưng cc6f3996af fix: only do 8 jobs at once in update & verify
Prevent lag which can lead to unusable system (especially when using hdiffpatch)

Also print more
2023-03-01 17:32:07 +07:00
tretrauit 1957f8265b fix: fix 2023-01-18 13:39:34 +07:00
tretrauit 820bc70e9d feat: hdiff in voiceover update 2023-01-18 12:38:43 +07:00
tretrauit 089b799a5f fix: RuntimeWarning: coroutine 'Installer.get_voiceover_archive_language' was never awaited 2023-01-18 12:25:12 +07:00
tretrauit f46902879a fix: aiohttp.client_exceptions.ClientPayloadError: Response payload is not completed 2023-01-18 11:45:40 +07:00
tretrauit 95dccf7241 feat: --download-chunk
Also bump to 2.2.9
2023-01-18 11:28:33 +07:00
tretrauit 178f513e52
updater: change the returncode check
Signed-off-by: tretrauit <tretrauit@gmail.com>
2022-12-07 13:04:48 +07:00
tretrauit 98898827e9
updater: more logs
Signed-off-by: tretrauit <tretrauit@gmail.com>
2022-12-07 13:03:19 +07:00
tretrauit 95f21e6d2a
updater: do not continue if file extraction failed
Signed-off-by: tretrauit <tretrauit@gmail.com>
2022-12-07 12:55:03 +07:00
tretrauit e55d6cafd6
updater: do not delete directory
I took a look at deletefiles.txt again and realized that they're all files, so we don't need to delete the entire directory

Signed-off-by: tretrauit <tretrauit@gmail.com>
2022-11-03 17:44:54 +07:00
tretrauit 727b7e9b44
updater: fix deleting the game root directory lmao 2022-11-02 12:56:53 +07:00
tretrauit 0b51be1649
updater: remove directory in deletefiles.txt too 2022-11-02 12:26:37 +07:00
tretrauit abdf19bc16
patcher: switch to Krock branch 2022-09-29 12:11:32 +07:00
tretrauit e0f3fadc9d
fix: don't use 7zip
Providing that it doesn't provide any benefits than ZipFile
2022-09-28 22:53:12 +07:00
tretrauit cd2aabea60
hack: workaround over Windows path 2022-09-28 22:00:23 +07:00
tretrauit 1830081cb0
fix: use 7z to extract game file 2022-09-28 21:19:20 +07:00
tretrauit f54c2f432b
fix: handle FileExistsError 2022-09-28 12:51:27 +07:00
tretrauit e3045f5150
fix: do not await is_file() 2022-09-28 12:26:54 +07:00
9 changed files with 235 additions and 123 deletions

View File

@ -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

View File

@ -1,3 +1,3 @@
aiohttp==3.8.1
aiohttp==3.8.3
appdirs~=1.4.4
aiopath~=0.6.10

View File

@ -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",

View File

@ -3,3 +3,5 @@ from worthless import launcher, installer
Launcher = launcher.Launcher
Installer = installer.Installer
__version__ = "2.2.22"

0
worthless/__main__.py Executable file → Normal file
View File

View File

@ -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):

View File

@ -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()

View File

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

View File

@ -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: