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:
parent
8b2d0cad8f
commit
a5659f7ff3
@ -1,3 +1,5 @@
|
|||||||
# worthless-launcher
|
# worthless-launcher
|
||||||
|
|
||||||
A worthless CLI launcher written in Python.
|
A worthless CLI launcher written in Python.
|
||||||
|
|
||||||
|
Check out its website at https://tretrauit.gitlab.io/worthless-launcher for more information.
|
@ -1,4 +1,3 @@
|
|||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
appdirs~=1.4.4
|
appdirs~=1.4.4
|
||||||
aiopath~=0.6.10
|
aiopath~=0.6.10
|
||||||
xdelta3~=0.0.5
|
|
2
setup.py
2
setup.py
@ -9,7 +9,7 @@ README = (HERE / "README.md").read_text()
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='worthless',
|
name='worthless',
|
||||||
version='1.3.1-2',
|
version='2.0.0',
|
||||||
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',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
import asyncio
|
||||||
|
from worthless import cli
|
||||||
|
|
||||||
from worthless import gui
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(cli.main())
|
||||||
if __name__ == '__main__':
|
|
||||||
gui.main()
|
|
||||||
|
@ -12,6 +12,9 @@ class Diff:
|
|||||||
self.voice_packs = voice_packs
|
self.voice_packs = voice_packs
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.path.split("/")[-1]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data):
|
def from_dict(data):
|
||||||
voice_packs = []
|
voice_packs = []
|
||||||
|
@ -14,6 +14,9 @@ class Latest:
|
|||||||
self.segments = segments
|
self.segments = segments
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.path.split("/")[-1]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data):
|
def from_dict(data):
|
||||||
voice_packs = []
|
voice_packs = []
|
||||||
|
@ -7,6 +7,9 @@ class Voicepack:
|
|||||||
self.md5 = md5
|
self.md5 = md5
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.path.split("/")[-1]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data):
|
def from_dict(data):
|
||||||
return Voicepack(data["language"], data["name"], data["path"], data["size"], data["md5"], data)
|
return Voicepack(data["language"], data["name"], data["path"], data["size"], data["md5"], data)
|
||||||
|
@ -22,145 +22,146 @@ class UI:
|
|||||||
def _ask(self, question):
|
def _ask(self, question):
|
||||||
if self._noconfirm:
|
if self._noconfirm:
|
||||||
# Fake dialog
|
# Fake dialog
|
||||||
print(question + " (y/n): y")
|
print(question + " [Y/n]:")
|
||||||
return True
|
return True
|
||||||
answer = ""
|
answer = ""
|
||||||
while answer.lower() not in ['y', 'n']:
|
while answer.lower() not in ['y', 'n', '']:
|
||||||
if answer != "":
|
if answer != "":
|
||||||
print("Invalid choice, please try again.")
|
print("Invalid choice, please try again.")
|
||||||
answer = input(question + " (y/n): ")
|
answer = input(question + " [Y/n]: ")
|
||||||
return answer.lower() == 'y'
|
return answer.lower() == 'y' or answer == ''
|
||||||
|
|
||||||
def override_game_version(self, version: str):
|
def override_game_version(self, version: str):
|
||||||
self._installer._version = version
|
self._installer._version = version
|
||||||
|
|
||||||
def get_game_version(self):
|
async def get_game_version(self):
|
||||||
print(self._installer.get_game_version())
|
print(await self._installer.get_game_version())
|
||||||
|
|
||||||
def block_telemetry(self):
|
async def block_telemetry(self):
|
||||||
print("Checking for available telemetry to block...")
|
print("Checking for available telemetry to block...")
|
||||||
try:
|
try:
|
||||||
asyncio.run(self._patcher.block_telemetry())
|
await self._patcher.block_telemetry()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("No telemetry to block.")
|
print("No telemetry to block.")
|
||||||
else:
|
else:
|
||||||
print("Telemetry blocked.")
|
print("Telemetry blocked.")
|
||||||
|
|
||||||
def check_telemetry(self):
|
async def check_telemetry(self):
|
||||||
block_status = asyncio.run(self._patcher.is_telemetry_blocked())
|
block_status = await self._patcher.is_telemetry_blocked()
|
||||||
if not block_status:
|
if not block_status:
|
||||||
print("Telemetry is blocked.")
|
print("Telemetry is blocked.")
|
||||||
else:
|
else:
|
||||||
print("Telemetry is not blocked, you need to block these hosts below.")
|
print("Telemetry is not blocked, you need to block these hosts below.")
|
||||||
for block in block_status:
|
for hosts in block_status:
|
||||||
print(block)
|
print(hosts)
|
||||||
|
|
||||||
def _update_from_archive(self, filepath):
|
async def _update_from_archive(self, filepath):
|
||||||
print("Reverting patches if patched...")
|
print("Reverting patches if patched...")
|
||||||
self._patcher.revert_patch(True)
|
await self._patcher.revert_patch(True)
|
||||||
print("Updating game from archive (this may takes some time)...")
|
print("Updating game from archive (This may takes some time)...")
|
||||||
asyncio.run(self._installer.update_game(filepath))
|
await self._installer.update_game(filepath)
|
||||||
self._installer.set_version_config()
|
self._installer.set_version_config()
|
||||||
|
|
||||||
def _install_from_archive(self, filepath, force_reinstall):
|
async def _install_from_archive(self, filepath, force_reinstall):
|
||||||
print("Installing game from archive (this may takes some time)...")
|
print("Installing game from archive (This may takes some time)...")
|
||||||
self._installer.install_game(filepath, force_reinstall)
|
print(filepath)
|
||||||
|
await self._installer.install_game(filepath, force_reinstall)
|
||||||
self._installer.set_version_config()
|
self._installer.set_version_config()
|
||||||
|
|
||||||
def _apply_voiceover_from_archive(self, filepath):
|
async def _apply_voiceover_from_archive(self, filepath):
|
||||||
print("Applying voiceover from archive (this may takes some time)...")
|
print("Applying voiceover from archive (This may takes some time)...")
|
||||||
self._installer.apply_voiceover(filepath)
|
print("Voiceover archive:", filepath)
|
||||||
|
await self._installer.apply_voiceover(filepath)
|
||||||
|
|
||||||
def install_voiceover_from_file(self, filepath):
|
async def install_voiceover_from_file(self, filepath):
|
||||||
print("Archive voiceover language: {} ({})".format(
|
print("Archive voiceover language: {} ({})".format(
|
||||||
self._installer.get_voiceover_archive_language(filepath),
|
await self._installer.get_voiceover_archive_language(filepath),
|
||||||
"Full archive" if self._installer.get_voiceover_archive_type(filepath) else "Update archive"))
|
"Full archive" if await self._installer.get_voiceover_archive_type(filepath) else "Update archive"))
|
||||||
if not self._ask("Do you want to apply this voiceover pack? ({})".format(filepath)):
|
if not self._ask("Do you want to apply this voiceover pack? ({})".format(filepath)):
|
||||||
print("Aborting apply process.")
|
print("Aborting apply process.")
|
||||||
return
|
return
|
||||||
self._apply_voiceover_from_archive(filepath)
|
await self._apply_voiceover_from_archive(filepath)
|
||||||
print("Voiceover applied successfully.")
|
print("Voiceover applied successfully.")
|
||||||
|
|
||||||
def revert_patch(self):
|
async def revert_patch(self):
|
||||||
print("Reverting patches...")
|
print("Reverting patches...")
|
||||||
self._patcher.revert_patch(True)
|
await self._patcher.revert_patch(True)
|
||||||
print("Patches reverted.")
|
print("Patches reverted.")
|
||||||
|
|
||||||
def patch_game(self, login_fix: bool = False):
|
async def patch_game(self, login_fix: bool = False):
|
||||||
print("NOTE: Hereby you are violating the game's Terms of Service!")
|
print("NOTE: Hereby you are violating the game's Terms of Service!")
|
||||||
print("Do not patch the game if you don't know what you are doing!")
|
print("Do not patch the game if you don't know what you are doing!")
|
||||||
if not self._ask("Do you want to patch the game? (This will overwrite your game files!)"):
|
if not self._ask("Do you want to patch the game? (This will overwrite your game files!)"):
|
||||||
print("Aborting patch process.")
|
print("Aborting patch process.")
|
||||||
return
|
return
|
||||||
self.block_telemetry()
|
await self.block_telemetry()
|
||||||
print("Updating patches...")
|
print("Updating patches...")
|
||||||
asyncio.run(self._patcher.download_patch())
|
await self._patcher.download_patch()
|
||||||
print("Patching game...")
|
print("Patching game...")
|
||||||
self._patcher.apply_patch(login_fix)
|
await self._patcher.apply_patch(login_fix)
|
||||||
print("Game patched.")
|
print("Game patched.")
|
||||||
print("Please refrain from sharing this project to public, thank you.")
|
print("Please refrain from sharing this project to public, thank you.")
|
||||||
|
|
||||||
def install_from_file(self, filepath):
|
async def install_from_file(self, filepath):
|
||||||
gamever = self._installer.get_game_version()
|
gamever = await self._installer.get_game_version()
|
||||||
print("Archive game version: " + self._installer.get_game_archive_version(filepath))
|
print("Archive game version:", await self._installer.get_game_archive_version(filepath))
|
||||||
if gamever:
|
if gamever:
|
||||||
print("Current game installation detected. ({})".format(self._installer.get_game_version()))
|
print("Current game installation detected. ({})".format(await self._installer.get_game_version()))
|
||||||
if not self._ask("Do you want to update the game? ({})".format(filepath)):
|
if not self._ask("Do you want to update the game? ({})".format(filepath)):
|
||||||
print("Aborting update process.")
|
print("Aborting update process.")
|
||||||
return
|
return
|
||||||
self._update_from_archive(filepath)
|
await self._update_from_archive(filepath)
|
||||||
print("Game updated successfully.")
|
print("Game updated successfully.")
|
||||||
else:
|
else:
|
||||||
print("No game installation detected.")
|
print("No game installation detected.")
|
||||||
if not self._ask("Do you want to install the game? ({})".format(filepath)):
|
if not self._ask("Do you want to install the game? ({})".format(filepath)):
|
||||||
print("Aborting installation process.")
|
print("Aborting installation process.")
|
||||||
return
|
return
|
||||||
self._install_from_archive(filepath, False)
|
await self._install_from_archive(filepath, False)
|
||||||
print("Game installed successfully.")
|
print("Game installed successfully.")
|
||||||
|
|
||||||
def download_patch(self):
|
async def download_patch(self):
|
||||||
print("Downloading patches...")
|
print("Downloading patches...")
|
||||||
asyncio.run(self._patcher.download_patch())
|
await self._patcher.download_patch()
|
||||||
|
|
||||||
def download_game(self):
|
async def download_game(self):
|
||||||
print("Downloading full game (This will take a long time)...")
|
print("Downloading full game (This will take a long time)...")
|
||||||
asyncio.run(self._installer.download_full_game())
|
await self._installer.download_full_game()
|
||||||
|
|
||||||
def download_game_update(self):
|
async def download_game_update(self):
|
||||||
print("Downloading game update (This will take a long time)...")
|
print("Downloading game update (This will take a long time)...")
|
||||||
asyncio.run(self._installer.download_game_update())
|
await self._installer.download_game_update()
|
||||||
|
|
||||||
def download_voiceover(self, languages: str):
|
async def download_voiceover(self, languages: str):
|
||||||
res_info = asyncio.run(self._launcher.get_resource_info())
|
res_info = await self._launcher.get_resource_info()
|
||||||
for lng in languages.split(" "):
|
for lng in languages.split(" "):
|
||||||
for vo in res_info.game.latest.voice_packs:
|
for vo in res_info.game.latest.voice_packs:
|
||||||
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
||||||
continue
|
continue
|
||||||
print("Downloading voiceover pack for {} (This will take a long time)...".format(lng))
|
print("Downloading voiceover pack for {} (This will take a long time)...".format(lng))
|
||||||
asyncio.run(self._installer.download_full_voiceover(lng))
|
await self._installer.download_full_voiceover(lng)
|
||||||
|
|
||||||
def download_voiceover_update(self, languages: str):
|
async def download_voiceover_update(self, languages: str):
|
||||||
res_info = asyncio.run(self._launcher.get_resource_info())
|
res_info = await self._launcher.get_resource_info()
|
||||||
for lng in languages.split(" "):
|
for lng in languages.split(" "):
|
||||||
for vo in res_info.game.latest.voice_packs:
|
for vo in res_info.game.latest.voice_packs:
|
||||||
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
||||||
continue
|
continue
|
||||||
print("Downloading voiceover update pack for {} (This will take a long time)...".format(lng))
|
print("Downloading voiceover update pack for {} (This will take a long time)...".format(lng))
|
||||||
asyncio.run(self._installer.download_voiceover_update(lng))
|
await self._installer.download_voiceover_update(lng)
|
||||||
|
|
||||||
def install_game(self, forced: bool = False):
|
async def install_game(self, forced: bool = False):
|
||||||
res_info = asyncio.run(self._launcher.get_resource_info())
|
res_info = await self._launcher.get_resource_info()
|
||||||
print("Latest game version: {}".format(res_info.game.latest.version))
|
print("Latest game version: {}".format(res_info.game.latest.version))
|
||||||
if not self._ask("Do you want to install the game?"):
|
if not self._ask("Do you want to install the game?"):
|
||||||
print("Aborting game installation process.")
|
print("Aborting game installation process.")
|
||||||
return
|
return
|
||||||
self.download_game()
|
await self.download_game()
|
||||||
print("Installing game...")
|
print("Game archive:", res_info.game.latest.get_name())
|
||||||
self._install_from_archive(self._installer.temp_path.joinpath(res_info.game.latest.name), forced)
|
await self._install_from_archive(self._installer.temp_path.joinpath(res_info.game.latest.get_name()), forced)
|
||||||
|
|
||||||
def install_voiceover(self, languages: str):
|
async def install_voiceover(self, languages: str):
|
||||||
res_info = asyncio.run(self._launcher.get_resource_info())
|
res_info = await self._launcher.get_resource_info()
|
||||||
print("Latest game version: {}".format(res_info.game.latest.version))
|
|
||||||
for lng in languages.split(" "):
|
for lng in languages.split(" "):
|
||||||
for vo in res_info.game.latest.voice_packs:
|
for vo in res_info.game.latest.voice_packs:
|
||||||
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
if not self._installer.voiceover_lang_translate(lng) == vo.language:
|
||||||
@ -169,19 +170,20 @@ class UI:
|
|||||||
print("Aborting voiceover installation process.")
|
print("Aborting voiceover installation process.")
|
||||||
return
|
return
|
||||||
print("Downloading voiceover pack (This will take a long time)...")
|
print("Downloading voiceover pack (This will take a long time)...")
|
||||||
asyncio.run(self._installer.download_full_voiceover(lng))
|
await self._installer.download_full_voiceover(lng)
|
||||||
print("Installing voiceover pack...")
|
await self._apply_voiceover_from_archive(
|
||||||
self._apply_voiceover_from_archive(self._installer.temp_path.joinpath(res_info.game.latest.name))
|
self._installer.temp_path.joinpath(vo.get_name())
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
def update_game(self):
|
async def update_game(self):
|
||||||
game_ver = self._installer.get_game_version()
|
game_ver = await self._installer.get_game_version()
|
||||||
if not game_ver:
|
if not game_ver:
|
||||||
self.install_game()
|
await self.install_game()
|
||||||
return
|
return
|
||||||
print("Current game installation detected. ({})".format(game_ver))
|
print("Current game installation detected: {}".format(game_ver))
|
||||||
diff_archive = asyncio.run(self._installer.get_game_diff_archive())
|
diff_archive = await self._installer.get_game_diff_archive()
|
||||||
res_info = asyncio.run(self._launcher.get_resource_info())
|
res_info = await self._launcher.get_resource_info()
|
||||||
if not diff_archive:
|
if not diff_archive:
|
||||||
print("No game updates available.")
|
print("No game updates available.")
|
||||||
return
|
return
|
||||||
@ -190,19 +192,24 @@ class UI:
|
|||||||
print("Aborting game update process.")
|
print("Aborting game update process.")
|
||||||
return
|
return
|
||||||
print("Downloading game update (This will take a long time)...")
|
print("Downloading game update (This will take a long time)...")
|
||||||
asyncio.run(self._installer.download_game_update())
|
await self._installer.download_game_update()
|
||||||
print("Installing game update...")
|
print("Installing game update...")
|
||||||
self.install_from_file(self._installer.temp_path.joinpath(res_info.game.latest.name))
|
await self.install_from_file(self._installer.temp_path.joinpath(res_info.game.latest.name))
|
||||||
|
|
||||||
def update_voiceover(self, languages: str):
|
async def update_voiceover(self, languages: str | list):
|
||||||
game_ver = self._installer.get_game_version()
|
if isinstance(languages, str):
|
||||||
|
languages = languages.split(" ")
|
||||||
|
game_ver = await self._installer.get_game_version()
|
||||||
if not game_ver:
|
if not game_ver:
|
||||||
self.install_voiceover(languages)
|
print("Couldn't detect current game installation, is game installed?")
|
||||||
return
|
return
|
||||||
print("Current game installation detected. ({})".format(game_ver))
|
installed_voiceovers = await self._installer.get_installed_voiceovers()
|
||||||
for lng in languages.split(" "):
|
print(f"Installed voiceovers: {None if installed_voiceovers == [] else ', '.join(installed_voiceovers)}")
|
||||||
diff_archive = asyncio.run(self._installer.get_voiceover_diff_archive(lng))
|
for lng in languages:
|
||||||
# res_info = asyncio.run(self._launcher.get_resource_info())
|
if self._installer.voiceover_lang_translate(lng, "locale") not in installed_voiceovers:
|
||||||
|
await self.install_voiceover(lng)
|
||||||
|
continue
|
||||||
|
diff_archive = await self._installer.get_voiceover_diff_archive(lng)
|
||||||
if not diff_archive:
|
if not diff_archive:
|
||||||
print("No voiceover updates available for {}.".format(lng))
|
print("No voiceover updates available for {}.".format(lng))
|
||||||
continue
|
continue
|
||||||
@ -210,19 +217,40 @@ class UI:
|
|||||||
print("Aborting this voiceover language update process.")
|
print("Aborting this voiceover language update process.")
|
||||||
continue
|
continue
|
||||||
print("Downloading voiceover update (This may takes some time)...")
|
print("Downloading voiceover update (This may takes some time)...")
|
||||||
asyncio.run(self._installer.download_voiceover_update(lng))
|
await self._installer.download_voiceover_update(lng)
|
||||||
print("Installing voiceover update for {}...".format(lng))
|
print("Installing voiceover update for {}...".format(lng))
|
||||||
self._apply_voiceover_from_archive(self._installer.temp_path.joinpath(diff_archive.name))
|
await self._apply_voiceover_from_archive(self._installer.temp_path.joinpath(diff_archive.get_name()))
|
||||||
|
|
||||||
def update_game_voiceover(self, languages: str):
|
async def update_game_voiceover(self, languages: str):
|
||||||
self.update_game()
|
await self.update_game()
|
||||||
self.update_voiceover(languages)
|
await self.update_voiceover(languages)
|
||||||
|
|
||||||
def interactive_ui(self):
|
async def update_all(self):
|
||||||
raise NotImplementedError()
|
await self.update_game()
|
||||||
|
await self.update_voiceover(await self._installer.get_installed_voiceovers())
|
||||||
|
|
||||||
|
async def verify_game(self):
|
||||||
|
game_ver = await self._installer.get_game_version()
|
||||||
|
if not game_ver:
|
||||||
|
print("Couldn't detect current game installation, is game installed?")
|
||||||
|
return
|
||||||
|
print("Verifying game contents... (This may takes a long time)")
|
||||||
|
failed_files = await self._installer.verify_game(ignore_mismatch=True)
|
||||||
|
if not failed_files:
|
||||||
|
print("All good.")
|
||||||
|
return
|
||||||
|
print("Some game files got corrupted (mismatch md5), uh oh.")
|
||||||
|
for file in failed_files:
|
||||||
|
print("{}: expected {}, actual {}".format(file[0], file[1], file[2]))
|
||||||
|
|
||||||
|
async def clear_cache(self):
|
||||||
|
if self._ask("Do you want to clear Installer cache (contains downloaded game files, etc)"):
|
||||||
|
await self._installer.clear_cache()
|
||||||
|
if self._ask("Do you want to clear Patcher cache (contains files used to patch)"):
|
||||||
|
await self._patcher.clear_cache()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
async def main():
|
||||||
default_dirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
default_dirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
||||||
parser = argparse.ArgumentParser(prog="worthless", description="A worthless launcher written in Python.")
|
parser = argparse.ArgumentParser(prog="worthless", description="A worthless launcher written in Python.")
|
||||||
parser.add_argument("-D", "--dir", action="store", type=Path, default=Path.cwd(),
|
parser.add_argument("-D", "--dir", action="store", type=Path, default=Path.cwd(),
|
||||||
@ -259,19 +287,15 @@ def main():
|
|||||||
parser.add_argument("-Rs", "--remove", action="store_true", help="Remove the game (if installed)")
|
parser.add_argument("-Rs", "--remove", action="store_true", help="Remove the game (if installed)")
|
||||||
parser.add_argument("-Rp", "--remove-patch", action="store_true", help="Revert the game patch (if patched)")
|
parser.add_argument("-Rp", "--remove-patch", action="store_true", help="Revert the game patch (if patched)")
|
||||||
parser.add_argument("-Rv", "--remove-voiceover", action="store_true", help="Remove a Voiceover pack (if installed)")
|
parser.add_argument("-Rv", "--remove-voiceover", action="store_true", help="Remove a Voiceover pack (if installed)")
|
||||||
|
parser.add_argument("-V", "--verify", action="store_true", help="Verify the game installation")
|
||||||
parser.add_argument("--get-game-version", action="store_true", help="Get the current game version")
|
parser.add_argument("--get-game-version", action="store_true", help="Get the current game version")
|
||||||
parser.add_argument("--no-overseas", action="store_true", help="Don't use overseas server")
|
parser.add_argument("--no-overseas", action="store_true", help="Don't use overseas server")
|
||||||
parser.add_argument("--check-telemetry", action="store_true", help="Check for the telemetry information")
|
parser.add_argument("--check-telemetry", action="store_true", help="Check for the telemetry information")
|
||||||
|
parser.add_argument("--clear-cache", action="store_true", help="Clear cache used by worthless")
|
||||||
parser.add_argument("--from-ver", action="store", help="Override the detected game version", type=str, default=None)
|
parser.add_argument("--from-ver", action="store", help="Override the detected game version", type=str, default=None)
|
||||||
parser.add_argument("--noconfirm", action="store_true",
|
parser.add_argument("--noconfirm", action="store_true",
|
||||||
help="Do not ask any for confirmation. (Ignored in interactive mode)")
|
help="Do not ask any for confirmation. (Ignored in interactive mode)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
interactive_mode = not args.install and not args.install_from_file and not args.patch and not args.update and not \
|
|
||||||
args.remove and not args.remove_patch and not args.remove_voiceover and not args.get_game_version and not \
|
|
||||||
args.install_voiceover_from_file and not args.update_voiceover and not args.download_game and not \
|
|
||||||
args.download_voiceover and not args.download_game_update and not args.download_voiceover_update and not \
|
|
||||||
args.install_voiceover_from_file and not args.update_all and not args.login_fix and not args.check_telemetry\
|
|
||||||
and not args.from_ver
|
|
||||||
if args.temporary_dir:
|
if args.temporary_dir:
|
||||||
args.temporary_dir.mkdir(parents=True, exist_ok=True)
|
args.temporary_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@ -293,50 +317,62 @@ def main():
|
|||||||
ui.override_game_version(args.from_ver)
|
ui.override_game_version(args.from_ver)
|
||||||
|
|
||||||
if args.get_game_version:
|
if args.get_game_version:
|
||||||
ui.get_game_version()
|
await ui.get_game_version()
|
||||||
|
|
||||||
if args.check_telemetry:
|
if args.check_telemetry:
|
||||||
ui.check_telemetry()
|
await ui.check_telemetry()
|
||||||
|
|
||||||
|
# Download
|
||||||
|
|
||||||
if args.download_game:
|
if args.download_game:
|
||||||
ui.download_game()
|
await ui.download_game()
|
||||||
|
|
||||||
if args.download_voiceover:
|
if args.download_voiceover:
|
||||||
ui.download_voiceover(args.download_voiceover)
|
await ui.download_voiceover(args.download_voiceover)
|
||||||
|
|
||||||
if args.download_game_update:
|
if args.download_game_update:
|
||||||
ui.download_game_update()
|
await ui.download_game_update()
|
||||||
|
|
||||||
if args.download_voiceover_update:
|
if args.download_voiceover_update:
|
||||||
ui.download_voiceover_update(args.download_voiceover_update)
|
await ui.download_voiceover_update(args.download_voiceover_update)
|
||||||
|
|
||||||
|
# Install
|
||||||
|
|
||||||
if args.install:
|
if args.install:
|
||||||
ui.install_game()
|
await ui.install_game()
|
||||||
|
|
||||||
if args.update_all:
|
|
||||||
raise NotImplementedError() # TODO
|
|
||||||
|
|
||||||
if args.update:
|
|
||||||
ui.update_game_voiceover(args.update)
|
|
||||||
|
|
||||||
if args.update_voiceover:
|
|
||||||
ui.update_voiceover(args.update_voiceover)
|
|
||||||
|
|
||||||
if args.install_from_file:
|
if args.install_from_file:
|
||||||
ui.install_from_file(args.install_from_file)
|
await ui.install_from_file(args.install_from_file)
|
||||||
|
|
||||||
if args.install_voiceover_from_file:
|
if args.install_voiceover_from_file:
|
||||||
ui.install_voiceover_from_file(args.install_voiceover_from_file)
|
await ui.install_voiceover_from_file(args.install_voiceover_from_file)
|
||||||
|
|
||||||
|
# Update
|
||||||
|
|
||||||
|
if args.update_all:
|
||||||
|
await ui.update_all()
|
||||||
|
|
||||||
|
if args.update:
|
||||||
|
await ui.update_game_voiceover(args.update)
|
||||||
|
|
||||||
|
if args.update_voiceover:
|
||||||
|
await ui.update_voiceover(args.update_voiceover)
|
||||||
|
|
||||||
|
# Patch
|
||||||
|
|
||||||
if args.patch:
|
if args.patch:
|
||||||
ui.patch_game(args.login_fix)
|
await ui.patch_game(args.login_fix)
|
||||||
|
|
||||||
if args.remove_patch:
|
if args.remove_patch:
|
||||||
ui.revert_patch()
|
await ui.revert_patch()
|
||||||
|
|
||||||
if interactive_mode:
|
# Verify
|
||||||
ui.interactive_ui()
|
|
||||||
|
|
||||||
|
if args.verify:
|
||||||
|
await ui.verify_game()
|
||||||
|
|
||||||
|
if args.clear_cache:
|
||||||
|
await ui.clear_cache()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
asyncio.run(main())
|
@ -1,5 +1,9 @@
|
|||||||
|
from appdirs import AppDirs
|
||||||
|
|
||||||
|
|
||||||
APP_NAME="worthless"
|
APP_NAME="worthless"
|
||||||
APP_AUTHOR="tretrauit"
|
APP_AUTHOR="tretrauit"
|
||||||
|
APPDIRS=AppDirs(APP_NAME, APP_AUTHOR)
|
||||||
LAUNCHER_API_URL_OS = "https://sdk-os-static.hoyoverse.com/hk4e_global/mdk/launcher/api"
|
LAUNCHER_API_URL_OS = "https://sdk-os-static.hoyoverse.com/hk4e_global/mdk/launcher/api"
|
||||||
LAUNCHER_API_URL_CN = "https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api"
|
LAUNCHER_API_URL_CN = "https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api"
|
||||||
HDIFFPATCH_GIT_URL="https://github.com/sisong/HDiffPatch"
|
HDIFFPATCH_GIT_URL="https://github.com/sisong/HDiffPatch"
|
||||||
|
@ -3,10 +3,9 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import platform
|
import platform
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import appdirs
|
|
||||||
import zipfile
|
import zipfile
|
||||||
import warnings
|
|
||||||
import json
|
import json
|
||||||
|
import hashlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from aiopath import AsyncPath
|
from aiopath import AsyncPath
|
||||||
@ -23,19 +22,21 @@ async def _download_file(file_url: str, file_name: str, file_path: Path | str, f
|
|||||||
:param file_name:
|
:param file_name:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
params = {}
|
headers = {}
|
||||||
file_path = AsyncPath(file_path).joinpath(file_name)
|
file_path = AsyncPath(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 await file_path.exists():
|
||||||
cur_len = len(await file_path.read_bytes())
|
cur_len = (await file_path.stat()).st_size
|
||||||
params |= {
|
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()
|
await file_path.touch()
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
rsp = await session.get(file_url, params=params, timeout=None)
|
rsp = await session.get(file_url, headers=headers, timeout=None)
|
||||||
|
if rsp.status == 416:
|
||||||
|
return
|
||||||
rsp.raise_for_status()
|
rsp.raise_for_status()
|
||||||
while True:
|
while True:
|
||||||
chunk = await rsp.content.read(chunks)
|
chunk = await rsp.content.read(chunks)
|
||||||
@ -51,7 +52,7 @@ class HDiffPatch:
|
|||||||
git_url = constants.HDIFFPATCH_GIT_URL
|
git_url = constants.HDIFFPATCH_GIT_URL
|
||||||
self._git_url = git_url
|
self._git_url = git_url
|
||||||
if not data_dir:
|
if not data_dir:
|
||||||
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
self._appdirs = constants.APPDIRS
|
||||||
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("HDiffPatch")
|
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("HDiffPatch")
|
||||||
self.data_path = Path(self._appdirs.user_data_dir).joinpath("Tools/HDiffPatch")
|
self.data_path = Path(self._appdirs.user_data_dir).joinpath("Tools/HDiffPatch")
|
||||||
else:
|
else:
|
||||||
@ -98,7 +99,7 @@ class HDiffPatch:
|
|||||||
hpatchz_name = "hpatchz" + (".exe" if platform.system() == "Windows" else "")
|
hpatchz_name = "hpatchz" + (".exe" if platform.system() == "Windows" else "")
|
||||||
return self._get_hdiffpatch_exec(hpatchz_name)
|
return self._get_hdiffpatch_exec(hpatchz_name)
|
||||||
|
|
||||||
async def patch_file(self, in_file, out_file, patch_file, wait=False):
|
async def patch_file(self, in_file, out_file, patch_file, error=False, wait=False):
|
||||||
hpatchz = self.get_hpatchz_executable()
|
hpatchz = self.get_hpatchz_executable()
|
||||||
if not hpatchz:
|
if not hpatchz:
|
||||||
raise RuntimeError("hpatchz executable not found")
|
raise RuntimeError("hpatchz executable not found")
|
||||||
@ -106,6 +107,8 @@ class HDiffPatch:
|
|||||||
if not wait:
|
if not wait:
|
||||||
return proc
|
return proc
|
||||||
await proc.wait()
|
await proc.wait()
|
||||||
|
if error and proc.returncode != 0:
|
||||||
|
raise RuntimeError(f"Patching failed, return code is {proc.returncode}")
|
||||||
return proc
|
return proc
|
||||||
|
|
||||||
def get_hdiffz_executable(self):
|
def get_hdiffz_executable(self):
|
||||||
@ -141,91 +144,33 @@ class HDiffPatch:
|
|||||||
await _download_file(url, name, self.temp_path, overwrite=True)
|
await _download_file(url, name, self.temp_path, overwrite=True)
|
||||||
if not extract:
|
if not extract:
|
||||||
return
|
return
|
||||||
archive = zipfile.ZipFile(self.temp_path.joinpath(name))
|
with zipfile.ZipFile(self.temp_path.joinpath(name), 'r') as f:
|
||||||
archive.extractall(self.data_path)
|
await asyncio.to_thread(f.extractall, path=self.data_path)
|
||||||
archive.close()
|
|
||||||
|
|
||||||
|
|
||||||
class Installer:
|
class Installer:
|
||||||
def _read_version_from_config(self):
|
def __init__(self, gamedir: str | Path | AsyncPath = AsyncPath.cwd(),
|
||||||
warnings.warn("This function is not reliable as upgrading game version from worthless\
|
overseas: bool = True, data_dir: str | Path | AsyncPath = None):
|
||||||
doesn't write the config.", DeprecationWarning)
|
if isinstance(gamedir, str | Path):
|
||||||
if not self._config_file.exists():
|
gamedir = AsyncPath(gamedir)
|
||||||
raise FileNotFoundError(f"Config file {self._config_file} not found")
|
|
||||||
cfg = ConfigParser()
|
|
||||||
cfg.read(str(self._config_file))
|
|
||||||
return cfg.get("General", "game_version")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def read_version_from_game_file(globalgamemanagers: Path | bytes):
|
|
||||||
"""
|
|
||||||
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
|
|
||||||
|
|
||||||
Uses `An Anime Game Launcher` method to read the version:
|
|
||||||
https://gitlab.com/KRypt0n_/an-anime-game-launcher/-/blob/main/src/ts/Game.ts#L26
|
|
||||||
|
|
||||||
:return: Game version (ex 1.0.0)
|
|
||||||
"""
|
|
||||||
if isinstance(globalgamemanagers, Path):
|
|
||||||
with globalgamemanagers.open("rb") as f:
|
|
||||||
data = f.read().decode("ascii", errors="ignore")
|
|
||||||
else:
|
|
||||||
data = globalgamemanagers.decode("ascii", errors="ignore")
|
|
||||||
result = re.search(r"([1-9]+\.[0-9]+\.[0-9]+)_[\d]+_[\d]+", data)
|
|
||||||
if not result:
|
|
||||||
raise ValueError("Could not find version in game file")
|
|
||||||
return result.group(1)
|
|
||||||
|
|
||||||
def get_game_data_name(self):
|
|
||||||
if self._overseas:
|
|
||||||
return "GenshinImpact_Data/"
|
|
||||||
else:
|
|
||||||
return "YuanShen_Data/"
|
|
||||||
|
|
||||||
def get_game_data_path(self):
|
|
||||||
return self._gamedir.joinpath(self.get_game_data_name())
|
|
||||||
|
|
||||||
def get_game_version(self):
|
|
||||||
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
|
|
||||||
if not globalgamemanagers.exists():
|
|
||||||
return
|
|
||||||
return self.read_version_from_game_file(globalgamemanagers)
|
|
||||||
|
|
||||||
def get_installed_voiceovers(self):
|
|
||||||
"""
|
|
||||||
Returns a list of installed voiceovers.
|
|
||||||
|
|
||||||
:return: List of installed voiceovers
|
|
||||||
"""
|
|
||||||
voiceovers = []
|
|
||||||
for file in self.get_game_data_path().joinpath("StreamingAssets/Audio/GeneratedSoundBanks/Windows").iterdir():
|
|
||||||
if file.is_dir():
|
|
||||||
voiceovers.append(file.name)
|
|
||||||
return voiceovers
|
|
||||||
|
|
||||||
def __init__(self, gamedir: str | Path = Path.cwd(), overseas: bool = True, data_dir: str | Path = None):
|
|
||||||
if isinstance(gamedir, str):
|
|
||||||
gamedir = Path(gamedir)
|
|
||||||
self._gamedir = gamedir
|
self._gamedir = gamedir
|
||||||
if not data_dir:
|
if not data_dir:
|
||||||
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
self._appdirs = constants.APPDIRS
|
||||||
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("Installer")
|
self.temp_path = AsyncPath(self._appdirs.user_cache_dir).joinpath("Installer")
|
||||||
else:
|
else:
|
||||||
if not isinstance(data_dir, Path):
|
if isinstance(data_dir, str | AsyncPath):
|
||||||
data_dir = Path(data_dir)
|
data_dir = AsyncPath(data_dir)
|
||||||
self.temp_path = data_dir.joinpath("Temp/Installer/")
|
self.temp_path = data_dir.joinpath("Temp/Installer/")
|
||||||
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")
|
||||||
self._config_file = config_file.resolve()
|
self._config_file = config_file
|
||||||
self._download_chunk = 8192
|
self._download_chunk = 8192
|
||||||
self._overseas = overseas
|
self._overseas = overseas
|
||||||
self._version = self.get_game_version()
|
self._version = None
|
||||||
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
|
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
|
||||||
self._hdiffpatch = HDiffPatch(data_dir=data_dir)
|
self._hdiffpatch = HDiffPatch(data_dir=data_dir)
|
||||||
self._config = LauncherConfig(self._config_file, self._version)
|
self._config = LauncherConfig(self._config_file, self._version)
|
||||||
|
self._game_version_re = re.compile(r"([1-9]+\.[0-9]+\.[0-9]+)_\d+_\d+")
|
||||||
def set_download_chunk(self, chunk: int):
|
|
||||||
self._download_chunk = chunk
|
|
||||||
|
|
||||||
async def _download_file(self, file_url: str, file_name: str, file_len: int = None, overwrite=False):
|
async def _download_file(self, file_url: str, file_name: str, file_len: int = None, overwrite=False):
|
||||||
"""
|
"""
|
||||||
@ -234,76 +179,143 @@ class Installer:
|
|||||||
:param file_name:
|
:param file_name:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await _download_file(file_url, file_name, self.temp_path, file_len=file_len, overwrite=overwrite)
|
await _download_file(file_url, file_name, self.temp_path, file_len=file_len, overwrite=overwrite,
|
||||||
|
chunks=self._download_chunk)
|
||||||
|
|
||||||
def get_game_archive_version(self, game_archive: str | Path):
|
async def read_version_from_config(self):
|
||||||
if not game_archive.exists():
|
if not await self._config_file.exists():
|
||||||
raise FileNotFoundError(f"Game archive {game_archive} not found")
|
raise FileNotFoundError(f"Config file {self._config_file} not found")
|
||||||
archive = zipfile.ZipFile(game_archive, 'r')
|
cfg = ConfigParser()
|
||||||
return self.read_version_from_game_file(archive.read(self.get_game_data_name() + "globalgamemanagers"))
|
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:
|
||||||
|
"""
|
||||||
|
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
|
||||||
|
|
||||||
|
Uses `An Anime Game Launcher` method to read the version:
|
||||||
|
https://gitlab.com/KRypt0n_/an-anime-game-launcher/-/blob/main/src/ts/Game.ts#L26
|
||||||
|
|
||||||
|
:return: Game version (ex 1.0.0)
|
||||||
|
"""
|
||||||
|
if isinstance(globalgamemanagers, Path | AsyncPath):
|
||||||
|
globalgamemanagers = AsyncPath(globalgamemanagers)
|
||||||
|
data = await globalgamemanagers.read_text("ascii", errors="ignore")
|
||||||
|
else:
|
||||||
|
data = globalgamemanagers.decode("ascii", errors="ignore")
|
||||||
|
result = self._game_version_re.search(data)
|
||||||
|
if not result:
|
||||||
|
raise ValueError("Could not find version in game file")
|
||||||
|
return result.group(1)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def voiceover_lang_translate(lang: str):
|
def voiceover_lang_translate(lang: str, base_language="game") -> str:
|
||||||
"""
|
"""
|
||||||
Translates the voiceover language to the language code used by the game.
|
Translates the voiceover language to the language code used by the game.
|
||||||
|
|
||||||
:param lang: Language to translate
|
:param lang: Language to translate
|
||||||
|
:param base_language: Base language type (game/locale/both)
|
||||||
:return: Language code
|
:return: Language code
|
||||||
"""
|
"""
|
||||||
match lang:
|
if base_language == "game" or base_language == "both":
|
||||||
case "English(US)":
|
match lang.lower():
|
||||||
|
case "english(us)":
|
||||||
return "en-us"
|
return "en-us"
|
||||||
case "Japanese":
|
case "japanese":
|
||||||
return "ja-jp"
|
return "ja-jp"
|
||||||
case "Chinese":
|
case "chinese":
|
||||||
return "zh-cn"
|
return "zh-cn"
|
||||||
case "Korean":
|
case "korean":
|
||||||
return "ko-kr"
|
return "ko-kr"
|
||||||
|
if base_language == "locale" or base_language == "both":
|
||||||
|
match lang.lower().replace("_", "-"):
|
||||||
|
case "en-us":
|
||||||
|
return "English(US)"
|
||||||
|
case "ja-jp":
|
||||||
|
return "Japanese"
|
||||||
|
case "zh-cn":
|
||||||
|
return "Chinese"
|
||||||
|
case "ko-kr":
|
||||||
|
return "Korean"
|
||||||
|
# If nothing else matches
|
||||||
return lang
|
return lang
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_voiceover_archive_language(voiceover_archive: str | Path):
|
async def get_voiceover_archive_language(voiceover_archive: str | Path | AsyncPath) -> str:
|
||||||
if isinstance(voiceover_archive, str):
|
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():
|
||||||
raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found")
|
raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found")
|
||||||
archive = zipfile.ZipFile(voiceover_archive, 'r')
|
with zipfile.ZipFile(voiceover_archive, 'r') as f:
|
||||||
archive_path = zipfile.Path(archive)
|
for file in zipfile.Path(f).iterdir():
|
||||||
for file in archive_path.iterdir():
|
|
||||||
if file.name.endswith("_pkg_version"):
|
if file.name.endswith("_pkg_version"):
|
||||||
return file.name.split("_")[1]
|
return file.name.split("_")[1]
|
||||||
|
|
||||||
def get_voiceover_archive_type(self, voiceover_archive: str | Path):
|
@staticmethod
|
||||||
vo_lang = self.get_voiceover_archive_language(voiceover_archive)
|
async def get_voiceover_archive_type(voiceover_archive: str | Path) -> bool:
|
||||||
archive = zipfile.ZipFile(voiceover_archive, 'r')
|
"""
|
||||||
archive_path = zipfile.Path(archive)
|
Gets voiceover archive type.
|
||||||
files = archive.read("Audio_{}_pkg_version".format(vo_lang)).decode().split("\n")
|
:param voiceover_archive:
|
||||||
|
:return: True if this is a full archive, else False.
|
||||||
|
"""
|
||||||
|
vo_lang = 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")
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.strip() and not archive_path.joinpath(json.loads(file)["remoteName"]).exists():
|
if file.strip() and not archive_path.joinpath(json.loads(file)["remoteName"]).exists():
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def apply_voiceover(self, voiceover_archive: str | Path):
|
def set_download_chunk(self, chunk: int):
|
||||||
# Since Voiceover packages are unclear about diff package or full package
|
self._download_chunk = chunk
|
||||||
# we will try to extract the voiceover package and apply it to the game
|
|
||||||
# making this function universal for both cases
|
|
||||||
if not self.get_game_data_path().exists():
|
|
||||||
raise FileNotFoundError(f"Game not found in {self._gamedir}")
|
|
||||||
if isinstance(voiceover_archive, str):
|
|
||||||
voiceover_archive = Path(voiceover_archive).resolve()
|
|
||||||
if not voiceover_archive.exists():
|
|
||||||
raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found")
|
|
||||||
archive = zipfile.ZipFile(voiceover_archive, 'r')
|
|
||||||
archive.extractall(self._gamedir)
|
|
||||||
archive.close()
|
|
||||||
|
|
||||||
async def update_game(self, game_archive: str | Path):
|
def get_game_data_name(self):
|
||||||
if not self.get_game_data_path().exists():
|
if self._overseas:
|
||||||
|
return "GenshinImpact_Data/"
|
||||||
|
else:
|
||||||
|
return "YuanShen_Data/"
|
||||||
|
|
||||||
|
def get_game_data_path(self) -> AsyncPath:
|
||||||
|
return self._gamedir.joinpath(self.get_game_data_name())
|
||||||
|
|
||||||
|
async def get_game_archive_version(self, game_archive: str | Path):
|
||||||
|
if not game_archive.exists():
|
||||||
|
raise FileNotFoundError(f"Game archive {game_archive} not found")
|
||||||
|
with zipfile.ZipFile(game_archive, 'r') as f:
|
||||||
|
return await self.read_version_from_game_file(
|
||||||
|
await asyncio.to_thread(f.read, self.get_game_data_name() + "globalgamemanagers")
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_game_version(self) -> str | None:
|
||||||
|
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
|
||||||
|
if not await globalgamemanagers.exists():
|
||||||
|
try:
|
||||||
|
return await self.read_version_from_config()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return
|
||||||
|
return await self.read_version_from_game_file(globalgamemanagers)
|
||||||
|
|
||||||
|
async def get_installed_voiceovers(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
Returns a list of installed voiceovers.
|
||||||
|
|
||||||
|
: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():
|
||||||
|
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}")
|
raise FileNotFoundError(f"Game not found in {self._gamedir}")
|
||||||
if isinstance(game_archive, str):
|
if isinstance(game_archive, str | Path):
|
||||||
game_archive = Path(game_archive).resolve()
|
game_archive = Path(game_archive).resolve()
|
||||||
if not game_archive.exists():
|
if not game_archive.exists():
|
||||||
raise FileNotFoundError(f"Update archive {game_archive} not found")
|
raise FileNotFoundError(f"Update archive {game_archive} not found")
|
||||||
|
|
||||||
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():
|
||||||
@ -320,13 +332,13 @@ class Installer:
|
|||||||
|
|
||||||
# hdiffpatch implementation
|
# hdiffpatch implementation
|
||||||
hdifffiles = []
|
hdifffiles = []
|
||||||
for x in archive.read("hdifffiles.txt").decode().split("\n"):
|
for x in (await asyncio.to_thread(archive.read, "hdifffiles.txt")).decode().split("\n"):
|
||||||
if x:
|
if x:
|
||||||
hdifffiles.append(json.loads(x)["remoteName"])
|
hdifffiles.append(json.loads(x)["remoteName"])
|
||||||
patch_jobs = []
|
patch_jobs = []
|
||||||
for file in hdifffiles:
|
for file in hdifffiles:
|
||||||
current_game_file = self._gamedir.joinpath(file)
|
current_game_file = self._gamedir.joinpath(file)
|
||||||
if not current_game_file.exists():
|
if not await current_game_file.exists():
|
||||||
# Not patching since we don't have the file
|
# Not patching since we don't have the file
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -341,10 +353,11 @@ class Installer:
|
|||||||
patch_path, wait=True)
|
patch_path, wait=True)
|
||||||
patch_path.unlink()
|
patch_path.unlink()
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
# Let the game redownload the file.
|
# Let the game download the file.
|
||||||
old_file.rename(old_file.with_suffix(old_suffix))
|
old_file.rename(old_file.with_suffix(old_suffix))
|
||||||
return
|
return
|
||||||
old_file.unlink()
|
old_file.unlink()
|
||||||
|
|
||||||
files.remove(patch_file)
|
files.remove(patch_file)
|
||||||
patch_jobs.append(extract_and_patch(current_game_file, patch_file))
|
patch_jobs.append(extract_and_patch(current_game_file, patch_file))
|
||||||
|
|
||||||
@ -353,15 +366,16 @@ class Installer:
|
|||||||
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
|
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
|
||||||
for file in deletefiles:
|
for file in deletefiles:
|
||||||
current_game_file = self._gamedir.joinpath(file)
|
current_game_file = self._gamedir.joinpath(file)
|
||||||
if not current_game_file.exists():
|
if not await current_game_file.exists():
|
||||||
continue
|
continue
|
||||||
if current_game_file.is_file():
|
if current_game_file.is_file():
|
||||||
current_game_file.unlink(missing_ok=True)
|
current_game_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
archive.extractall(self._gamedir, members=files)
|
await asyncio.to_thread(archive.extractall, self._gamedir, members=files)
|
||||||
archive.close()
|
archive.close()
|
||||||
# Update game version on local variable.
|
# Update game version on local variable.
|
||||||
self._version = self.get_game_version()
|
self._version = await self.get_game_version()
|
||||||
|
self.set_version_config()
|
||||||
|
|
||||||
def set_version_config(self, version: str = None):
|
def set_version_config(self, version: str = None):
|
||||||
if not version:
|
if not version:
|
||||||
@ -370,11 +384,11 @@ class Installer:
|
|||||||
self._config.save()
|
self._config.save()
|
||||||
|
|
||||||
async def download_full_game(self):
|
async def download_full_game(self):
|
||||||
archive = await self._launcher.get_resource_info()
|
resource = await self._launcher.get_resource_info()
|
||||||
if archive is None:
|
if resource is None:
|
||||||
raise RuntimeError("Failed to fetch game resource info.")
|
raise RuntimeError("Failed to fetch game resource info.")
|
||||||
archive_name = archive.game.latest.path.split("/")[-1]
|
archive_name = resource.game.latest.path.split("/")[-1]
|
||||||
await self._download_file(archive.game.latest.path, archive_name, archive.game.latest.size)
|
await self._download_file(resource.game.latest.path, archive_name, resource.game.latest.size)
|
||||||
|
|
||||||
async def download_full_voiceover(self, language: str):
|
async def download_full_voiceover(self, language: str):
|
||||||
archive = await self._launcher.get_resource_info()
|
archive = await self._launcher.get_resource_info()
|
||||||
@ -383,19 +397,57 @@ class Installer:
|
|||||||
translated_lang = self.voiceover_lang_translate(language)
|
translated_lang = self.voiceover_lang_translate(language)
|
||||||
for vo in archive.game.latest.voice_packs:
|
for vo in archive.game.latest.voice_packs:
|
||||||
if vo.language == translated_lang:
|
if vo.language == translated_lang:
|
||||||
await self._download_file(vo.path, vo.name, vo.size)
|
await self._download_file(vo.path, vo.get_name(), vo.size)
|
||||||
|
|
||||||
async def download_game_update(self, from_version: str = None):
|
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):
|
||||||
|
archive = Path(archive).resolve()
|
||||||
|
if not archive.exists():
|
||||||
|
raise FileNotFoundError(f"'{archive}' not found")
|
||||||
|
with zipfile.ZipFile(archive, 'r') as f:
|
||||||
|
await asyncio.to_thread(f.extractall, path=self._gamedir)
|
||||||
|
|
||||||
|
async def apply_voiceover(self, voiceover_archive: str | Path):
|
||||||
|
# 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():
|
||||||
|
raise FileNotFoundError(f"Game not found in {self._gamedir}")
|
||||||
|
await self._extract_game_file(voiceover_archive)
|
||||||
|
|
||||||
|
async def install_game(self, game_archive: str | Path | AsyncPath, 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 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)
|
||||||
|
await self._extract_game_file(game_archive)
|
||||||
|
self._version = await self.get_game_version()
|
||||||
|
self.set_version_config()
|
||||||
|
|
||||||
|
async def _get_game_resource(self, from_version: str = None):
|
||||||
if not from_version:
|
if not from_version:
|
||||||
if self._version:
|
if self._version:
|
||||||
from_version = self._version
|
from_version = self._version
|
||||||
else:
|
else:
|
||||||
from_version = self._version = self.get_game_version()
|
from_version = self._version = await self.get_game_version()
|
||||||
if not from_version:
|
if not from_version:
|
||||||
raise ValueError("No game version found")
|
raise ValueError("No game version found")
|
||||||
version_info = await self._launcher.get_resource_info()
|
game_resource = await self._launcher.get_resource_info()
|
||||||
if version_info is None:
|
if not game_resource:
|
||||||
raise RuntimeError("Failed to fetch game resource info.")
|
raise ValueError("Could not fetch game resource")
|
||||||
|
return game_resource
|
||||||
|
|
||||||
|
async def download_game_update(self, from_version: str = None):
|
||||||
|
version_info = await self._get_game_resource()
|
||||||
if self._version == version_info.game.latest.version:
|
if self._version == version_info.game.latest.version:
|
||||||
raise ValueError("Game is already up to date.")
|
raise ValueError("Game is already up to date.")
|
||||||
diff_archive = await self.get_game_diff_archive(from_version)
|
diff_archive = await self.get_game_diff_archive(from_version)
|
||||||
@ -404,56 +456,17 @@ class Installer:
|
|||||||
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
|
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
|
||||||
|
|
||||||
async def download_voiceover_update(self, language: str, from_version: str = None):
|
async def download_voiceover_update(self, language: str, from_version: str = None):
|
||||||
if not from_version:
|
|
||||||
if self._version:
|
|
||||||
from_version = self._version
|
|
||||||
else:
|
|
||||||
from_version = self._version = self.get_game_version()
|
|
||||||
if not from_version:
|
|
||||||
raise ValueError("No game version found")
|
|
||||||
version_info = await self._launcher.get_resource_info()
|
|
||||||
if version_info is None:
|
|
||||||
raise RuntimeError("Failed to fetch game resource info.")
|
|
||||||
diff_archive = await self.get_voiceover_diff_archive(language, from_version)
|
diff_archive = await self.get_voiceover_diff_archive(language, from_version)
|
||||||
if diff_archive is None:
|
if diff_archive is None:
|
||||||
raise ValueError("Voiceover diff archive is not available for this version, please reinstall.")
|
raise ValueError("Voiceover diff archive is not available for this version, please reinstall.")
|
||||||
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
|
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
|
||||||
|
|
||||||
def uninstall_game(self):
|
|
||||||
shutil.rmtree(self._gamedir)
|
|
||||||
|
|
||||||
def install_game(self, game_archive: str | 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 self.get_game_data_path().exists():
|
|
||||||
if not force_reinstall:
|
|
||||||
raise ValueError(f"Game is already installed in {self._gamedir}")
|
|
||||||
self.uninstall_game()
|
|
||||||
|
|
||||||
self._gamedir.mkdir(parents=True, exist_ok=True)
|
|
||||||
if isinstance(game_archive, str):
|
|
||||||
game_archive = Path(game_archive).resolve()
|
|
||||||
if not game_archive.exists():
|
|
||||||
raise FileNotFoundError(f"Install archive {game_archive} not found")
|
|
||||||
archive = zipfile.ZipFile(game_archive, 'r')
|
|
||||||
archive.extractall(self._gamedir)
|
|
||||||
archive.close()
|
|
||||||
|
|
||||||
async def get_voiceover_diff_archive(self, lang: str, from_version: str = None):
|
async def get_voiceover_diff_archive(self, lang: str, from_version: str = None):
|
||||||
"""Gets a diff archive from `from_version` to the latest one
|
"""Gets a diff archive from `from_version` to the latest one
|
||||||
|
|
||||||
If from_version is not specified, it will be taken from the game version.
|
If from_version is not specified, it will be taken from the game version.
|
||||||
"""
|
"""
|
||||||
if not from_version:
|
game_resource = await self._get_game_resource()
|
||||||
if self._version:
|
|
||||||
from_version = self._version
|
|
||||||
else:
|
|
||||||
from_version = self._version = self.get_game_version()
|
|
||||||
if not from_version:
|
|
||||||
raise ValueError("No game version found")
|
|
||||||
game_resource = await self._launcher.get_resource_info()
|
|
||||||
if not game_resource:
|
if not game_resource:
|
||||||
raise ValueError("Could not fetch game resource")
|
raise ValueError("Could not fetch game resource")
|
||||||
translated_lang = self.voiceover_lang_translate(lang)
|
translated_lang = self.voiceover_lang_translate(lang)
|
||||||
@ -470,16 +483,48 @@ class Installer:
|
|||||||
|
|
||||||
If from_version is not specified, it will be taken from the game version.
|
If from_version is not specified, it will be taken from the game version.
|
||||||
"""
|
"""
|
||||||
if not from_version:
|
game_resource = await self._get_game_resource()
|
||||||
if self._version:
|
|
||||||
from_version = self._version
|
|
||||||
else:
|
|
||||||
from_version = self._version = self.get_game_version()
|
|
||||||
if not from_version:
|
|
||||||
raise ValueError("No game version found")
|
|
||||||
game_resource = await self._launcher.get_resource_info()
|
|
||||||
if not game_resource:
|
|
||||||
raise ValueError("Could not fetch game resource")
|
|
||||||
for v in game_resource.game.diffs:
|
for v in game_resource.game.diffs:
|
||||||
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):
|
||||||
|
contents = await pkg_version.read_text()
|
||||||
|
|
||||||
|
async def calculate_md5(file_to_calculate):
|
||||||
|
async with AsyncPath(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_file(file_to_verify, md5):
|
||||||
|
file_md5 = await calculate_md5(file_to_verify)
|
||||||
|
if file_md5 == md5:
|
||||||
|
return None
|
||||||
|
if ignore_mismatch:
|
||||||
|
return file_to_verify, md5, file_md5
|
||||||
|
raise ValueError(f"MD5 does not match for {file_to_verify}, expected md5: {md5}, actual md5: {file_md5}")
|
||||||
|
|
||||||
|
verify_jobs = []
|
||||||
|
for content in contents.split("\r\n"):
|
||||||
|
if not content.strip():
|
||||||
|
continue
|
||||||
|
info = json.loads(content)
|
||||||
|
verify_jobs.append(verify_file(self._gamedir.joinpath(info["remoteName"]), info["md5"]))
|
||||||
|
|
||||||
|
verify_result = await asyncio.gather(*verify_jobs)
|
||||||
|
failed_files = []
|
||||||
|
for file in verify_result:
|
||||||
|
if file is not None:
|
||||||
|
failed_files.append(file)
|
||||||
|
|
||||||
|
return None if not failed_files else failed_files
|
||||||
|
|
||||||
|
async def verify_game(self, pkg_version: str | Path | AsyncPath = 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)
|
||||||
|
|
||||||
|
async def clear_cache(self):
|
||||||
|
await asyncio.to_thread(shutil.rmtree, self.temp_path, ignore_errors=True)
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
|
from aiopath import AsyncPath
|
||||||
|
|
||||||
from worthless import constants
|
from worthless import constants
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from worthless.classes import launcher, installer
|
from worthless.classes import launcher, installer
|
||||||
@ -60,8 +63,8 @@ class Launcher:
|
|||||||
"launcher_id": "18",
|
"launcher_id": "18",
|
||||||
"channel_id": "1"
|
"channel_id": "1"
|
||||||
}
|
}
|
||||||
self._lang = "zh-cn" # Use chinese language because this is Pooh version
|
self._lang = "zh-cn" # Use chinese language because this is chinese version
|
||||||
if isinstance(gamedir, str):
|
if isinstance(gamedir, str | AsyncPath):
|
||||||
gamedir = Path(gamedir)
|
gamedir = Path(gamedir)
|
||||||
self._gamedir = gamedir.resolve()
|
self._gamedir = gamedir.resolve()
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from aiopath import AsyncPath
|
||||||
|
|
||||||
|
|
||||||
class LauncherConfig:
|
class LauncherConfig:
|
||||||
"""
|
"""
|
||||||
@ -23,8 +25,8 @@ class LauncherConfig:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
def __init__(self, config_path, game_version=None, overseas=True):
|
def __init__(self, config_path, game_version=None, overseas=True):
|
||||||
if isinstance(config_path, str):
|
if isinstance(config_path, str | AsyncPath):
|
||||||
self.config_path = Path(config_path)
|
config_path = Path(config_path)
|
||||||
if not game_version:
|
if not game_version:
|
||||||
game_version = "0.0.0"
|
game_version = "0.0.0"
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from aiopath import AsyncPath
|
||||||
|
|
||||||
|
|
||||||
class LinuxUtils:
|
class LinuxUtils:
|
||||||
@ -8,9 +9,12 @@ class LinuxUtils:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def _exec_command(self, args):
|
@staticmethod
|
||||||
|
async def _exec_command(args):
|
||||||
"""Execute a command using pkexec (friendly gui)
|
"""Execute a command using pkexec (friendly gui)
|
||||||
"""
|
"""
|
||||||
|
if not await AsyncPath("/usr/bin/pkexec").exists():
|
||||||
|
raise FileNotFoundError("pkexec not found.")
|
||||||
rsp = await asyncio.create_subprocess_shell(args)
|
rsp = await asyncio.create_subprocess_shell(args)
|
||||||
await rsp.wait()
|
await rsp.wait()
|
||||||
match rsp.returncode:
|
match rsp.returncode:
|
||||||
@ -21,14 +25,14 @@ class LinuxUtils:
|
|||||||
|
|
||||||
return rsp
|
return rsp
|
||||||
|
|
||||||
async def write_text_to_file(self, text, file_path: str | Path):
|
async def write_text_to_file(self, text, file_path: str | Path | AsyncPath):
|
||||||
"""Write text to a file using pkexec (friendly gui)
|
"""Write text to a file using pkexec (friendly gui)
|
||||||
"""
|
"""
|
||||||
if isinstance(file_path, Path):
|
if isinstance(file_path, Path | AsyncPath):
|
||||||
file_path = str(file_path)
|
file_path = str(file_path)
|
||||||
await self._exec_command('echo -e "{}" | pkexec tee {}'.format(text, file_path))
|
await self._exec_command('echo -e "{}" | pkexec tee {}'.format(text, file_path))
|
||||||
|
|
||||||
async def append_text_to_file(self, text, file_path: str | Path):
|
async def append_text_to_file(self, text, file_path: str | Path | AsyncPath):
|
||||||
"""Append text to a file using pkexec (friendly gui)
|
"""Append text to a file using pkexec (friendly gui)
|
||||||
"""
|
"""
|
||||||
if isinstance(file_path, Path):
|
if isinstance(file_path, Path):
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import tarfile
|
import tarfile
|
||||||
import appdirs
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from aiopath import AsyncPath
|
||||||
|
|
||||||
from worthless import constants
|
from worthless import constants
|
||||||
from worthless.launcher import Launcher
|
from worthless.launcher import Launcher
|
||||||
from worthless.installer import Installer
|
from worthless.installer import Installer
|
||||||
@ -26,16 +28,19 @@ except ImportError:
|
|||||||
|
|
||||||
|
|
||||||
class Patcher:
|
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._gamedir = gamedir
|
||||||
self._patch_url = (patch_url if patch_url else constants.PATCH_GIT_URL).replace('http://', 'https://')
|
self._patch_url = (patch_url if patch_url else constants.PATCH_GIT_URL).replace('http://', 'https://')
|
||||||
if not data_dir:
|
if not data_dir:
|
||||||
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
self._appdirs = constants.APPDIRS
|
||||||
self._patch_path = Path(self._appdirs.user_data_dir).joinpath("Patch")
|
self._patch_path = AsyncPath(self._appdirs.user_data_dir).joinpath("Patch")
|
||||||
self._temp_path = Path(self._appdirs.user_cache_dir).joinpath("Patcher")
|
self._temp_path = AsyncPath(self._appdirs.user_cache_dir).joinpath("Patcher")
|
||||||
else:
|
else:
|
||||||
if not isinstance(data_dir, Path):
|
if isinstance(data_dir, str | Path):
|
||||||
data_dir = Path(data_dir)
|
data_dir = AsyncPath(data_dir)
|
||||||
self._patch_path = data_dir.joinpath("Patch")
|
self._patch_path = data_dir.joinpath("Patch")
|
||||||
self._temp_path = data_dir.joinpath("Temp/Patcher")
|
self._temp_path = data_dir.joinpath("Temp/Patcher")
|
||||||
self._overseas = overseas
|
self._overseas = overseas
|
||||||
@ -43,7 +48,7 @@ class Patcher:
|
|||||||
self._launcher = Launcher(self._gamedir, overseas=overseas)
|
self._launcher = Launcher(self._gamedir, overseas=overseas)
|
||||||
match platform.system():
|
match platform.system():
|
||||||
case "Linux":
|
case "Linux":
|
||||||
self._linuxutils = linux.LinuxUtils()
|
self._linux = linux.LinuxUtils()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _get(url, **kwargs) -> aiohttp.ClientResponse:
|
async def _get(url, **kwargs) -> aiohttp.ClientResponse:
|
||||||
@ -76,15 +81,15 @@ class Patcher:
|
|||||||
else:
|
else:
|
||||||
return await archive.read()
|
return await archive.read()
|
||||||
|
|
||||||
async def _download_repo(self):
|
async def _download_repo(self, fallback=False):
|
||||||
if shutil.which("git"):
|
if shutil.which("git") and not fallback:
|
||||||
if not self._patch_path.exists() or not self._patch_path.is_dir() \
|
if not await self._patch_path.is_dir() or not await self._patch_path.joinpath(".git").exists():
|
||||||
or not self._patch_path.joinpath(".git").exists():
|
|
||||||
proc = await asyncio.create_subprocess_exec("git", "clone", self._patch_url, str(self._patch_path))
|
proc = await asyncio.create_subprocess_exec("git", "clone", self._patch_url, str(self._patch_path))
|
||||||
await proc.wait()
|
|
||||||
else:
|
else:
|
||||||
proc = await asyncio.create_subprocess_exec("git", "pull", cwd=str(self._patch_path))
|
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:
|
else:
|
||||||
archive = await self._get_git_archive()
|
archive = await self._get_git_archive()
|
||||||
if not archive:
|
if not archive:
|
||||||
@ -120,24 +125,18 @@ class Patcher:
|
|||||||
telemetry_url = constants.TELEMETRY_URL_LIST
|
telemetry_url = constants.TELEMETRY_URL_LIST
|
||||||
else:
|
else:
|
||||||
telemetry_url = constants.TELEMETRY_URL_CN_LIST
|
telemetry_url = constants.TELEMETRY_URL_CN_LIST
|
||||||
|
if optional:
|
||||||
|
telemetry_url |= constants.TELEMETRY_OPTIONAL_URL_LIST
|
||||||
unblocked_list = []
|
unblocked_list = []
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
for url in telemetry_url:
|
for url in telemetry_url:
|
||||||
try:
|
try:
|
||||||
await session.get("https://" + url)
|
await session.get("https://" + url, timeout=15)
|
||||||
except (aiohttp.ClientResponseError, aiohttp.ClientConnectorError):
|
except (aiohttp.ClientResponseError, aiohttp.ClientConnectorError, asyncio.exceptions.TimeoutError):
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
unblocked_list.append(url)
|
unblocked_list.append(url)
|
||||||
if optional:
|
return None if not unblocked_list else unblocked_list
|
||||||
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
|
|
||||||
|
|
||||||
async def block_telemetry(self, optional=False):
|
async def block_telemetry(self, optional=False):
|
||||||
telemetry = await self.is_telemetry_blocked(optional)
|
telemetry = await self.is_telemetry_blocked(optional)
|
||||||
@ -148,20 +147,15 @@ class Patcher:
|
|||||||
telemetry_hosts += "0.0.0.0 " + url + "\n"
|
telemetry_hosts += "0.0.0.0 " + url + "\n"
|
||||||
match platform.system():
|
match platform.system():
|
||||||
case "Linux":
|
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
|
return
|
||||||
# TODO: Windows and macOS
|
# TODO: Windows and macOS
|
||||||
raise NotImplementedError("Platform not implemented.")
|
raise NotImplementedError("Platform not implemented.")
|
||||||
|
|
||||||
async def _patch_unityplayer_fallback(self):
|
async def _patch_unityplayer_fallback(self, patch):
|
||||||
# xdelta3-python doesn't work because it's outdated.
|
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||||
if self._overseas:
|
|
||||||
patch = "unityplayer_patch_os.vcdiff"
|
|
||||||
else:
|
|
||||||
patch = "unityplayer_patch_cn.vcdiff"
|
|
||||||
gamever = "".join(self._installer.get_game_version().split("."))
|
|
||||||
unity_path = self._gamedir.joinpath("UnityPlayer.dll")
|
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",
|
proc = await asyncio.create_subprocess_exec("xdelta3", "-d", "-s",
|
||||||
str(self._gamedir.joinpath("UnityPlayer.dll.bak")),
|
str(self._gamedir.joinpath("UnityPlayer.dll.bak")),
|
||||||
str(self._patch_path.joinpath(
|
str(self._patch_path.joinpath(
|
||||||
@ -169,16 +163,11 @@ class Patcher:
|
|||||||
str(self._gamedir.joinpath("UnityPlayer.dll")), cwd=self._gamedir)
|
str(self._gamedir.joinpath("UnityPlayer.dll")), cwd=self._gamedir)
|
||||||
await proc.wait()
|
await proc.wait()
|
||||||
|
|
||||||
async def _patch_xlua_fallback(self):
|
async def _patch_xlua_fallback(self, patch):
|
||||||
# xdelta3-python doesn't work becuase it's outdated.
|
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||||
if self._overseas:
|
|
||||||
patch = "unityplayer_patch_os.vcdiff"
|
|
||||||
else:
|
|
||||||
patch = "unityplayer_patch_cn.vcdiff"
|
|
||||||
gamever = "".join(self._installer.get_game_version().split("."))
|
|
||||||
data_name = self._installer.get_game_data_name()
|
data_name = self._installer.get_game_data_name()
|
||||||
xlua_path = self._gamedir.joinpath("{}/Plugins/xlua.dll".format(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",
|
proc = await asyncio.create_subprocess_exec("xdelta3", "-d", "-s",
|
||||||
str(self._gamedir.joinpath(
|
str(self._gamedir.joinpath(
|
||||||
"{}/Plugins/xlua.dll.bak".format(data_name))),
|
"{}/Plugins/xlua.dll.bak".format(data_name))),
|
||||||
@ -189,12 +178,8 @@ class Patcher:
|
|||||||
cwd=self._gamedir)
|
cwd=self._gamedir)
|
||||||
await proc.wait()
|
await proc.wait()
|
||||||
|
|
||||||
def _patch_unityplayer(self):
|
async def _patch_unityplayer(self, patch):
|
||||||
if self._overseas:
|
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||||
patch = "unityplayer_patch_os.vcdiff"
|
|
||||||
else:
|
|
||||||
patch = "unityplayer_patch_cn.vcdiff"
|
|
||||||
gamever = "".join(self._installer.get_game_version().split("."))
|
|
||||||
unity_path = self._gamedir.joinpath("UnityPlayer.dll")
|
unity_path = self._gamedir.joinpath("UnityPlayer.dll")
|
||||||
patch_bytes = self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch)).read_bytes()
|
patch_bytes = self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch)).read_bytes()
|
||||||
patched_unity_bytes = xdelta3.decode(unity_path.read_bytes(), patch_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:
|
with Path(self._gamedir.joinpath("UnityPlayer.dll")).open("wb") as f:
|
||||||
f.write(patched_unity_bytes)
|
f.write(patched_unity_bytes)
|
||||||
|
|
||||||
def _patch_xlua(self):
|
async def _patch_xlua(self, patch):
|
||||||
patch = "xlua_patch.vcdiff"
|
gamever = "".join((await self._installer.get_game_version()).split("."))
|
||||||
gamever = "".join(self._installer.get_game_version().split("."))
|
|
||||||
data_name = self._installer.get_game_data_name()
|
data_name = self._installer.get_game_data_name()
|
||||||
xlua_path = self._gamedir.joinpath("{}/Plugins/xlua.dll".format(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()
|
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:
|
with Path(self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))).open("wb") as f:
|
||||||
f.write(patched_xlua_bytes)
|
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:
|
if NO_XDELTA3_MODULE or fallback:
|
||||||
asyncio.run(self._patch_xlua_fallback())
|
await self._patch_xlua_fallback(patch)
|
||||||
return
|
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)
|
Patch the game (and optionally patch xLua if specified)
|
||||||
|
|
||||||
@ -228,14 +216,22 @@ class Patcher:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Patch UnityPlayer.dll
|
# Patch UnityPlayer.dll
|
||||||
if NO_XDELTA3_MODULE or fallback:
|
# xdelta3-python doesn't work because it's outdated.
|
||||||
asyncio.run(self._patch_unityplayer_fallback())
|
if self._overseas:
|
||||||
|
patch = "unityplayer_patch_os.vcdiff"
|
||||||
else:
|
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
|
# Patch xLua.dll
|
||||||
if crash_fix:
|
if crash_fix:
|
||||||
self.apply_xlua_patch(fallback=fallback)
|
patch_jobs.append(self.apply_xlua_patch(fallback=fallback))
|
||||||
# Disable crash reporters
|
# Disable crash reporters
|
||||||
|
|
||||||
|
async def disable_crashreporters():
|
||||||
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",
|
||||||
@ -245,8 +241,11 @@ class Patcher:
|
|||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
file_path.rename(str(file_path) + ".bak")
|
file_path.rename(str(file_path) + ".bak")
|
||||||
|
|
||||||
|
patch_jobs.append(disable_crashreporters())
|
||||||
|
await asyncio.gather(*patch_jobs)
|
||||||
|
|
||||||
@staticmethod
|
@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
|
Try to get the date that a file was created, falling back to when it was
|
||||||
last modified if that isn't possible.
|
last modified if that isn't possible.
|
||||||
@ -255,7 +254,7 @@ class Patcher:
|
|||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
return os.path.getctime(file_path)
|
return os.path.getctime(file_path)
|
||||||
else:
|
else:
|
||||||
stat = file_path.stat()
|
stat = await file_path.stat()
|
||||||
try:
|
try:
|
||||||
return stat.st_birthtime
|
return stat.st_birthtime
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -263,11 +262,11 @@ class Patcher:
|
|||||||
# so we'll settle for when its content was last modified.
|
# so we'll settle for when its content was last modified.
|
||||||
return stat.st_mtime
|
return stat.st_mtime
|
||||||
|
|
||||||
def _revert_file(self, original_file: str, base_file: Path, ignore_error=False):
|
async def _revert_file(self, original_file: str, base_file: AsyncPath, ignore_error=False):
|
||||||
original_path = self._gamedir.joinpath(original_file + ".bak").resolve()
|
original_path = await self._gamedir.joinpath(original_file + ".bak").resolve()
|
||||||
target_file = self._gamedir.joinpath(original_file).resolve()
|
target_file = await self._gamedir.joinpath(original_file).resolve()
|
||||||
if original_path.exists():
|
if await original_path.exists():
|
||||||
if abs(self._creation_date(base_file) - self._creation_date(original_path)) > 86400: # 24 hours
|
if abs(await self._creation_date(base_file) - await self._creation_date(original_path)) > 86400: # 24 hours
|
||||||
if not ignore_error:
|
if not ignore_error:
|
||||||
raise RuntimeError("{} is not for this game version.".format(original_path.name))
|
raise RuntimeError("{} is not for this game version.".format(original_path.name))
|
||||||
original_path.unlink(missing_ok=True)
|
original_path.unlink(missing_ok=True)
|
||||||
@ -275,30 +274,36 @@ class Patcher:
|
|||||||
target_file.unlink(missing_ok=True)
|
target_file.unlink(missing_ok=True)
|
||||||
original_path.rename(target_file)
|
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
|
: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 = [
|
revert_files = [
|
||||||
"UnityPlayer.dll",
|
"UnityPlayer.dll",
|
||||||
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",
|
||||||
]
|
]
|
||||||
|
revert_job = []
|
||||||
for file in revert_files:
|
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"]:
|
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 = []
|
all_files = []
|
||||||
for ext in extensions:
|
for ext in extensions:
|
||||||
all_files.extend(self._gamedir.glob(ext))
|
all_files.extend(await self._gamedir.glob(ext))
|
||||||
return all_files
|
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:
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user