Compare commits

...

10 Commits

Author SHA1 Message Date
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
5e46b23752 chore: delete files first to save space 2023-05-25 13:03:05 +07:00
973ae2a5e6 fix: actually delete files in deletefiles.txt 2023-05-25 13:01:04 +07:00
aa4fe4d5ed chore: bump to 2.2.19 2023-05-25 10:42:14 +07:00
836c843b2e fix: str -> Path 2023-05-25 10:41:51 +07:00
0af4c4f2df chore: bump 2023-05-25 10:39:59 +07:00
a6600cf573 fix: wrong blueReporter path 2023-05-25 10:39:40 +07:00
305021d8b7 chore: bump to 2.2.17 2023-05-25 10:38:25 +07:00
2d5c75109a fix: add blueReporter to revert list 2023-05-25 10:29:24 +07:00
45006ef4b5 fix: telemetry.dll rename 2023-05-25 10:28:17 +07:00
5 changed files with 81 additions and 58 deletions

View File

@ -10,7 +10,7 @@ README = (HERE / "README.md").read_text()
setup( setup(
name='worthless', name='worthless',
version='2.2.16', version='2.2.20',
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',

View File

@ -4,4 +4,4 @@ Launcher = launcher.Launcher
Installer = installer.Installer Installer = installer.Installer
__version__ = "2.2.16" __version__ = "2.2.20"

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

View File

@ -9,7 +9,6 @@ from configparser import ConfigParser
from pathlib import Path from pathlib import Path
import aiohttp import aiohttp
from aiopath import AsyncPath
from worthless import constants from worthless import constants
from worthless.launcher import Launcher from worthless.launcher import Launcher
@ -25,16 +24,16 @@ async def _download_file(file_url: str, file_name: str, file_path: Path | str, f
:return: :return:
""" """
headers = {} headers = {}
file_path = AsyncPath(file_path).joinpath(file_name) file_path = Path(file_path).joinpath(file_name)
if overwrite: if overwrite:
await file_path.unlink(missing_ok=True) await file_path.unlink(missing_ok=True)
if await file_path.exists(): if file_path.exists():
cur_len = (await file_path.stat()).st_size cur_len = (file_path.stat()).st_size
headers |= { headers |= {
"Range": f"bytes={cur_len}-{file_len if file_len else ''}" "Range": f"bytes={cur_len}-{file_len if file_len else ''}"
} }
else: else:
await file_path.touch() file_path.touch()
print(f"Downloading {file_url} to {file_path}...") print(f"Downloading {file_url} to {file_path}...")
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=60*60, sock_read=240)) as session: 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) rsp = await session.get(file_url, headers=headers, timeout=None)
@ -46,8 +45,8 @@ async def _download_file(file_url: str, file_name: str, file_path: Path | str, f
await asyncio.sleep(0) await asyncio.sleep(0)
if not chunk: if not chunk:
break break
async with file_path.open("ab") as f: with file_path.open("ab") as f:
await f.write(chunk) f.write(chunk)
def calculate_md5(file_to_calculate): def calculate_md5(file_to_calculate):
@ -167,17 +166,17 @@ class HDiffPatch:
class Installer: class Installer:
def __init__(self, gamedir: str | Path | AsyncPath = AsyncPath.cwd(), def __init__(self, gamedir: str | Path = Path.cwd(),
overseas: bool = True, data_dir: str | Path | AsyncPath = None): overseas: bool = True, data_dir: str | Path = None):
if isinstance(gamedir, str | Path): if isinstance(gamedir, str | Path):
gamedir = AsyncPath(gamedir) gamedir = Path(gamedir)
self._gamedir = gamedir self._gamedir = gamedir
if not data_dir: if not data_dir:
self._appdirs = constants.APPDIRS 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: else:
if isinstance(data_dir, str | AsyncPath): if isinstance(data_dir, str | Path):
data_dir = AsyncPath(data_dir) data_dir = Path(data_dir)
self.temp_path = data_dir.joinpath("Temp/Installer/") self.temp_path = data_dir.joinpath("Temp/Installer/")
Path(self.temp_path).mkdir(parents=True, exist_ok=True) Path(self.temp_path).mkdir(parents=True, exist_ok=True)
config_file = self._gamedir.joinpath("config.ini") config_file = self._gamedir.joinpath("config.ini")
@ -201,13 +200,13 @@ class Installer:
chunks=self._download_chunk) chunks=self._download_chunk)
async def read_version_from_config(self): 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") raise FileNotFoundError(f"Config file {self._config_file} not found")
cfg = ConfigParser() cfg = ConfigParser()
await asyncio.to_thread(cfg.read, str(self._config_file)) await asyncio.to_thread(cfg.read, str(self._config_file))
return cfg.get("General", "game_version") 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) Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
@ -216,9 +215,9 @@ class Installer:
:return: Game version (ex 1.0.0) :return: Game version (ex 1.0.0)
""" """
if isinstance(globalgamemanagers, Path | AsyncPath): if isinstance(globalgamemanagers, Path | Path):
globalgamemanagers = AsyncPath(globalgamemanagers) globalgamemanagers = Path(globalgamemanagers)
data = await globalgamemanagers.read_text("ascii", errors="ignore") data = globalgamemanagers.read_text("ascii", errors="ignore")
else: else:
data = globalgamemanagers.decode("ascii", errors="ignore") data = globalgamemanagers.decode("ascii", errors="ignore")
result = self._game_version_re.search(data) result = self._game_version_re.search(data)
@ -258,7 +257,7 @@ class Installer:
return lang return lang
@staticmethod @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): if isinstance(voiceover_archive, str | Path):
voiceover_archive = Path(voiceover_archive).resolve() voiceover_archive = Path(voiceover_archive).resolve()
if not voiceover_archive.exists(): if not voiceover_archive.exists():
@ -293,7 +292,7 @@ class Installer:
else: else:
return "YuanShen_Data/" 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()) return self._gamedir.joinpath(self.get_game_data_name())
async def get_game_archive_version(self, game_archive: str | Path): async def get_game_archive_version(self, game_archive: str | Path):
@ -307,7 +306,7 @@ class Installer:
async def get_game_version(self) -> str | None: async def get_game_version(self) -> str | None:
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers") globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
if not await globalgamemanagers.exists(): if not globalgamemanagers.exists():
try: try:
return await self.read_version_from_config() return await self.read_version_from_config()
except FileNotFoundError: except FileNotFoundError:
@ -323,11 +322,11 @@ class Installer:
voiceovers = [] voiceovers = []
async for file in self.get_game_data_path()\ async for file in self.get_game_data_path()\
.joinpath("StreamingAssets/AudioAssets/").iterdir(): .joinpath("StreamingAssets/AudioAssets/").iterdir():
if await file.is_dir(): if file.is_dir():
voiceovers.append(file.name) voiceovers.append(file.name)
return voiceovers return voiceovers
async def _update(self, game_archive: str | Path | AsyncPath): async def _update(self, game_archive: str | Path | Path):
archive = zipfile.ZipFile(game_archive, 'r') archive = zipfile.ZipFile(game_archive, 'r')
if not self._hdiffpatch.get_hpatchz_executable(): if not self._hdiffpatch.get_hpatchz_executable():
@ -343,6 +342,22 @@ class Installer:
except ValueError: except ValueError:
pass 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 # hdiffpatch implementation
try: try:
hdifffiles = [] hdifffiles = []
@ -354,7 +369,7 @@ class Installer:
count = 0 count = 0
for file in hdifffiles: for file in hdifffiles:
current_game_file = self._gamedir.joinpath(file) current_game_file = self._gamedir.joinpath(file)
if not await current_game_file.exists(): if not current_game_file.exists():
print("File", file, "not found") print("File", file, "not found")
# Not patching since we don't have the file # Not patching since we don't have the file
continue continue
@ -371,18 +386,18 @@ class Installer:
print("Failed to extract diff file", diff_file) print("Failed to extract diff file", diff_file)
return return
old_suffix = old_file.suffix old_suffix = old_file.suffix
old_file = await old_file.rename(old_file.with_suffix(".bak")) old_file = old_file.rename(old_file.with_suffix(".bak"))
proc = await self._hdiffpatch.patch_file(old_file, old_file.with_suffix(old_suffix), proc = await self._hdiffpatch.patch_file(old_file, old_file.with_suffix(old_suffix),
patch_path, wait=True) patch_path, wait=True)
patch_path.unlink() patch_path.unlink()
if proc.returncode == 0: if proc.returncode == 0:
await old_file.unlink() old_file.unlink()
return return
# Let the game download the file. # Let the game download the file.
print("Failed to patch {}, reverting and let the in-game updater do the job...".format( print("Failed to patch {}, reverting and let the in-game updater do the job...".format(
old_file.with_suffix(old_suffix)) old_file.with_suffix(old_suffix))
) )
await old_file.rename(old_file.with_suffix(old_suffix)) old_file.rename(old_file.with_suffix(old_suffix))
files.remove(patch_file) files.remove(patch_file)
# Limit to 8 process running so it doesn't hang the PC. # Limit to 8 process running so it doesn't hang the PC.
@ -403,24 +418,11 @@ class Installer:
except Exception as e: except Exception as e:
print(f"Error while reading hdifffiles.txt: {e}") print(f"Error while reading hdifffiles.txt: {e}")
try:
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
for file in deletefiles:
current_game_file = Path(self._gamedir.joinpath(file))
if current_game_file == Path(self._gamedir):
continue
if not current_game_file.is_file():
continue
print("Deleting ", file)
current_game_file.unlink(missing_ok=True)
except Exception as e:
print(f"Error while reading deletefiles.txt: {e}")
await asyncio.to_thread(archive.extractall, self._gamedir, members=files) await asyncio.to_thread(archive.extractall, self._gamedir, members=files)
archive.close() archive.close()
async def update_game(self, game_archive: str | Path | AsyncPath): async def update_game(self, game_archive: str | Path | Path):
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}") raise FileNotFoundError(f"Game not found in {self._gamedir}")
if isinstance(game_archive, str | Path): if isinstance(game_archive, str | Path):
game_archive = Path(game_archive).resolve() game_archive = Path(game_archive).resolve()
@ -440,10 +442,24 @@ class Installer:
async def download_full_game(self, pre_download=False): async def download_full_game(self, pre_download=False):
game = await self._get_game(pre_download) game = await self._get_game(pre_download)
archive_name = game.latest.path.split("/")[-1] if not game.latest.path == "":
await self._download_file(game.latest.path, archive_name, game.latest.size) archive_name = game.latest.path.split("/")[-1]
if calculate_md5(self.temp_path.joinpath(archive_name)) != game.latest.md5: if calculate_md5(self.temp_path.joinpath(archive_name)) != game.latest.md5:
raise RuntimeError("mismatch md5 for downloaded game archive") 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
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()
async def download_full_voiceover(self, language: str, pre_download=False): async def download_full_voiceover(self, language: str, pre_download=False):
game = await self._get_game(pre_download) game = await self._get_game(pre_download)
@ -455,8 +471,8 @@ class Installer:
async def uninstall_game(self): async def uninstall_game(self):
await asyncio.to_thread(shutil.rmtree, self._gamedir, ignore_errors=True) await asyncio.to_thread(shutil.rmtree, self._gamedir, ignore_errors=True)
async def _extract_game_file(self, archive: str | Path | AsyncPath): async def _extract_game_file(self, archive: str | Path | Path):
if isinstance(archive, str | AsyncPath): if isinstance(archive, str | Path):
archive = Path(archive).resolve() archive = Path(archive).resolve()
if not archive.exists(): if not archive.exists():
raise FileNotFoundError(f"'{archive}' not found") raise FileNotFoundError(f"'{archive}' not found")
@ -467,24 +483,24 @@ class Installer:
# Since Voiceover packages are unclear about diff package or full package # 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 # we will try to extract the voiceover package and apply it to the game
# making this function universal for both cases # 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}") raise FileNotFoundError(f"Game not found in {self._gamedir}")
if isinstance(voiceover_archive, str | Path): if isinstance(voiceover_archive, str | Path):
voiceover_archive = Path(voiceover_archive).resolve() voiceover_archive = Path(voiceover_archive).resolve()
await self._update(voiceover_archive) await self._update(voiceover_archive)
# await self._extract_game_file(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 """Installs the game to the current directory
If `force_reinstall` is True, the game will be uninstalled then reinstalled. 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: if not force_reinstall:
raise ValueError(f"Game is already installed in {self._gamedir}") raise ValueError(f"Game is already installed in {self._gamedir}")
await self.uninstall_game() 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) await self._extract_game_file(game_archive)
self._version = await self.get_game_version() self._version = await self.get_game_version()
self.set_version_config() self.set_version_config()
@ -556,8 +572,8 @@ class Installer:
if v.version == from_version: if v.version == from_version:
return v return v
async def verify_from_pkg_version(self, pkg_version: AsyncPath, ignore_mismatch=False): async def verify_from_pkg_version(self, pkg_version: Path, ignore_mismatch=False):
contents = await pkg_version.read_text() contents = pkg_version.read_text()
async def verify_file(file_to_verify, md5): async def verify_file(file_to_verify, md5):
print("Verifying file:", file_to_verify) print("Verifying file:", file_to_verify)
@ -593,7 +609,7 @@ class Installer:
return None if not failed_files else failed_files 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: if pkg_version is None:
pkg_version = self._gamedir.joinpath("pkg_version") pkg_version = self._gamedir.joinpath("pkg_version")
return await self.verify_from_pkg_version(pkg_version, ignore_mismatch) return await self.verify_from_pkg_version(pkg_version, ignore_mismatch)

View File

@ -237,13 +237,19 @@ class Patcher:
disable_files = [ disable_files = [
self._installer.get_game_data_name() + "upload_crash.exe", 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/crashreport.exe",
self._installer.get_game_data_name() + "Plugins/blueReporter.exe", self._installer.get_game_data_name() + "blueReporter.exe",
] ]
for file in disable_files: for file in disable_files:
file_path = Path(self._gamedir.joinpath(file)).resolve() file_path = Path(self._gamedir.joinpath(file)).resolve()
if file_path.exists(): if file_path.exists():
await AsyncPath(file_path).rename(str(file_path) + ".bak") 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()) patch_jobs.append(disable_crashreporters())
await asyncio.gather(*patch_jobs) await asyncio.gather(*patch_jobs)
@ -291,6 +297,7 @@ class Patcher:
self._installer.get_game_data_name() + "upload_crash.exe", 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/crashreport.exe",
self._installer.get_game_data_name() + "Plugins/xlua.dll", self._installer.get_game_data_name() + "Plugins/xlua.dll",
self._installer.get_game_data_name() + "blueReporter.exe",
] ]
revert_job = [] revert_job = []
for file in revert_files: for file in revert_files: