Support chinese variant, QQ object in launcher and some code optimizations
This commit is contained in:
parent
048a7ac9d0
commit
c22918673b
80
.gitignore
vendored
80
.gitignore
vendored
@ -152,3 +152,83 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
_trial_temp/
|
||||||
|
_trail_temp.lock
|
||||||
|
3
.idea/.gitignore
vendored
Normal file
3
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) <year> <copyright holders>
|
Copyright (c) 2022 tretrauit
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
# worthless-launcher
|
# worthless-launcher
|
||||||
|
|
||||||
A worthless CLI launcher.
|
A worthless CLI launcher written in Python.
|
@ -1 +1,3 @@
|
|||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
|
appdirs~=1.4.4
|
||||||
|
aiofiles~=0.8.0
|
38
tests/launcher_api_cn_test.py
Normal file
38
tests/launcher_api_cn_test.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import unittest
|
||||||
|
import asyncio
|
||||||
|
import worthless
|
||||||
|
from worthless.classes import launcher
|
||||||
|
client = worthless.Launcher(overseas=False)
|
||||||
|
|
||||||
|
|
||||||
|
class LauncherCNTest(unittest.TestCase):
|
||||||
|
def test_get_version_info(self):
|
||||||
|
version_info = asyncio.run(client.get_version_info())
|
||||||
|
print("get_version_info test.")
|
||||||
|
print("get_version_info: ", version_info)
|
||||||
|
self.assertIsInstance(version_info, dict)
|
||||||
|
|
||||||
|
def test_get_launcher_info(self):
|
||||||
|
launcher_info = asyncio.run(client.get_launcher_info())
|
||||||
|
print("get_launcher_info test.")
|
||||||
|
print("get_launcher_info: ", launcher_info)
|
||||||
|
print("raw: ", launcher_info.raw)
|
||||||
|
self.assertIsInstance(launcher_info, launcher.Info)
|
||||||
|
|
||||||
|
def test_get_launcher_full_info(self):
|
||||||
|
launcher_info = asyncio.run(client.get_launcher_full_info())
|
||||||
|
print("get_launcher_full_info test.")
|
||||||
|
print("get_launcher_full_info: ", launcher_info)
|
||||||
|
print("raw: ", launcher_info.raw)
|
||||||
|
self.assertIsInstance(launcher_info, launcher.Info)
|
||||||
|
|
||||||
|
def test_get_launcher_background_url(self):
|
||||||
|
bg_url = asyncio.run(client.get_launcher_background_url())
|
||||||
|
print("get_launcher_background_url test.")
|
||||||
|
print("get_launcher_background_url: ", bg_url)
|
||||||
|
self.assertIsInstance(bg_url, str)
|
||||||
|
self.assertTrue(bg_url)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
@ -5,7 +5,7 @@ from worthless.classes import launcher
|
|||||||
client = worthless.Launcher()
|
client = worthless.Launcher()
|
||||||
|
|
||||||
|
|
||||||
class LauncherTest(unittest.TestCase):
|
class LauncherOverseasTest(unittest.TestCase):
|
||||||
def test_get_version_info(self):
|
def test_get_version_info(self):
|
||||||
version_info = asyncio.run(client.get_version_info())
|
version_info = asyncio.run(client.get_version_info())
|
||||||
print("get_version_info test.")
|
print("get_version_info test.")
|
||||||
@ -16,12 +16,14 @@ class LauncherTest(unittest.TestCase):
|
|||||||
launcher_info = asyncio.run(client.get_launcher_info())
|
launcher_info = asyncio.run(client.get_launcher_info())
|
||||||
print("get_launcher_info test.")
|
print("get_launcher_info test.")
|
||||||
print("get_launcher_info: ", launcher_info)
|
print("get_launcher_info: ", launcher_info)
|
||||||
|
print("raw: ", launcher_info.raw)
|
||||||
self.assertIsInstance(launcher_info, launcher.Info)
|
self.assertIsInstance(launcher_info, launcher.Info)
|
||||||
|
|
||||||
def test_get_launcher_full_info(self):
|
def test_get_launcher_full_info(self):
|
||||||
launcher_info = asyncio.run(client.get_launcher_full_info())
|
launcher_info = asyncio.run(client.get_launcher_full_info())
|
||||||
print("get_launcher_full_info test.")
|
print("get_launcher_full_info test.")
|
||||||
print("get_launcher_full_info: ", launcher_info)
|
print("get_launcher_full_info: ", launcher_info)
|
||||||
|
print("raw: ", launcher_info.raw)
|
||||||
self.assertIsInstance(launcher_info, launcher.Info)
|
self.assertIsInstance(launcher_info, launcher.Info)
|
||||||
|
|
||||||
def test_get_launcher_background_url(self):
|
def test_get_launcher_background_url(self):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from worthless import gui
|
import gui as gui
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
gui.main()
|
gui.main()
|
||||||
|
@ -3,7 +3,7 @@ class Background:
|
|||||||
|
|
||||||
Note that the `background` variable is an url to the background image,
|
Note that the `background` variable is an url to the background image,
|
||||||
while the `url` variable contains an empty string, so it seems that the
|
while the `url` variable contains an empty string, so it seems that the
|
||||||
`url` and `icon` variables are used by the official launcher itself.
|
`url` and `icon` variables are not used by the official launcher itself.
|
||||||
|
|
||||||
Also, the launcher background checksum is using an algorithm which I
|
Also, the launcher background checksum is using an algorithm which I
|
||||||
haven't found out yet, so you better not rely on it but instead rely
|
haven't found out yet, so you better not rely on it but instead rely
|
||||||
@ -16,8 +16,9 @@ class Background:
|
|||||||
- :class:`str` url: The url variable.
|
- :class:`str` url: The url variable.
|
||||||
- :class:`str` version: The launcher background version.
|
- :class:`str` version: The launcher background version.
|
||||||
- :class:`str` bg_checksum: The launcher background checksum.
|
- :class:`str` bg_checksum: The launcher background checksum.
|
||||||
- :class:`dict` raw: The launcher background raw information in dict.
|
- :class:`dict` raw: The launcher background raw information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, background, icon, url, version, bg_checksum, raw):
|
def __init__(self, background, icon, url, version, bg_checksum, raw):
|
||||||
"""Inits the launcher background class"""
|
"""Inits the launcher background class"""
|
||||||
self.background = background
|
self.background = background
|
||||||
|
@ -16,9 +16,10 @@ class IconButton:
|
|||||||
- :class:`str` qr_img: The QR code url.
|
- :class:`str` qr_img: The QR code url.
|
||||||
- :class:`str` qr_desc: The QR code description.
|
- :class:`str` qr_desc: The QR code description.
|
||||||
- :class:`str` img_hover: The icon url when hovered over.
|
- :class:`str` img_hover: The icon url when hovered over.
|
||||||
- :class:`dict[LauncherIconOtherLink]` other_links: Other links in the button.
|
- :class:`list[LauncherIconOtherLink]` other_links: Other links in the button.
|
||||||
- :class:`dict` raw: The launcher background raw information in dict.
|
- :class:`dict` raw: The launcher background raw information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, icon_id, img, tittle, url, qr_img, qr_desc, img_hover, other_links, raw):
|
def __init__(self, icon_id, img, tittle, url, qr_img, qr_desc, img_hover, other_links, raw):
|
||||||
"""Inits the launcher icon class"""
|
"""Inits the launcher icon class"""
|
||||||
self.icon_id = icon_id
|
self.icon_id = icon_id
|
||||||
@ -39,4 +40,3 @@ class IconButton:
|
|||||||
other_links.append(IconOtherLink.from_dict(link))
|
other_links.append(IconOtherLink.from_dict(link))
|
||||||
return IconButton(data["icon_id"], data["img"], data["tittle"], data["url"], data["qr_img"],
|
return IconButton(data["icon_id"], data["img"], data["tittle"], data["url"], data["qr_img"],
|
||||||
data["qr_desc"], data["img_hover"], other_links, data)
|
data["qr_desc"], data["img_hover"], other_links, data)
|
||||||
|
|
||||||
|
@ -1,16 +1,33 @@
|
|||||||
from worthless.classes.launcher import background, banner, iconbutton, post
|
from worthless.classes.launcher import background, banner, iconbutton, post, qq
|
||||||
Background = background.Background
|
Background = background.Background
|
||||||
Banner = banner.Banner
|
Banner = banner.Banner
|
||||||
IconButton = iconbutton.IconButton
|
IconButton = iconbutton.IconButton
|
||||||
Post = post.Post
|
Post = post.Post
|
||||||
|
QQ = qq.QQ
|
||||||
|
|
||||||
|
|
||||||
class Info:
|
class Info:
|
||||||
def __init__(self, lc_background: Background, lc_banner: list[Banner], icon: list[IconButton], lc_post: list[Post]):
|
"""Contains the launcher information
|
||||||
|
|
||||||
|
Note that QQ is not wrapped due to not having access to the chinese yuanshen launcher.
|
||||||
|
You can contribute to the project if you want :D
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
|
||||||
|
- :class:`worthless.classes.launcher.background.Background` background: The launcher background information.
|
||||||
|
- :class:`worthless.classes.launcher.banner.Banner` banner: The launcher banner information.
|
||||||
|
- :class:`worthless.classes.launcher.iconbutton.IconButton` icon: The launcher icon buttons information.
|
||||||
|
- :class:`worthless.classes.launcher.qq.QQ` post: The launcher QQ posts information.
|
||||||
|
- :class:`dict` raw: The launcher raw information.
|
||||||
|
"""
|
||||||
|
def __init__(self, lc_background: Background, lc_banner: list[Banner], icon: list[IconButton], lc_post: list[Post],
|
||||||
|
lc_qq: list[QQ], raw: dict):
|
||||||
self.background = lc_background
|
self.background = lc_background
|
||||||
self.banner = lc_banner
|
self.banner = lc_banner
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.post = lc_post
|
self.post = lc_post
|
||||||
|
self.qq = lc_qq
|
||||||
|
self.raw = raw
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data):
|
def from_dict(data):
|
||||||
@ -24,5 +41,8 @@ class Info:
|
|||||||
lc_post = []
|
lc_post = []
|
||||||
for p in data["post"]:
|
for p in data["post"]:
|
||||||
lc_post.append(Post.from_dict(p))
|
lc_post.append(Post.from_dict(p))
|
||||||
return Info(bg, lc_banner, lc_icon, lc_post)
|
lc_qq = []
|
||||||
|
for q in data["qq"]:
|
||||||
|
lc_qq.append(QQ.from_dict(q))
|
||||||
|
return Info(bg, lc_banner, lc_icon, lc_post, lc_qq, data)
|
||||||
|
|
||||||
|
@ -13,13 +13,13 @@ class Post:
|
|||||||
Attributes:
|
Attributes:
|
||||||
|
|
||||||
- :class:`str` post_id: The launcher post id.
|
- :class:`str` post_id: The launcher post id.
|
||||||
- :class:`str` type: The post type, as explained above.
|
- :class:`str` type: The post type, can be POST_TYPE_ANNOUNCE, POST_TYPE_ACTIVITY and POST_TYPE_INFO
|
||||||
- :class:`str` tittle: The post title.
|
- :class:`str` tittle: The post title.
|
||||||
- :class:`str` url: The post target url.
|
- :class:`str` url: The post target url.
|
||||||
- :class:`str` show_time: The time when the post will be shown.
|
- :class:`str` show_time: The time when the post will be shown.
|
||||||
- :class:`str` order: The post order.
|
- :class:`str` order: The post order.
|
||||||
- :class:`str` title: The post title.
|
- :class:`str` title: The post title.
|
||||||
- :class:`dict` raw: The banner raw information.
|
- :class:`dict` raw: The post raw information.
|
||||||
"""
|
"""
|
||||||
def __init__(self, post_id, post_type, tittle, url, show_time, order, title, raw):
|
def __init__(self, post_id, post_type, tittle, url, show_time, order, title, raw):
|
||||||
self.post_id = post_id
|
self.post_id = post_id
|
||||||
|
35
worthless/classes/launcher/qq.py
Normal file
35
worthless/classes/launcher/qq.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
class QQ:
|
||||||
|
"""Contains the launcher QQ information
|
||||||
|
|
||||||
|
Note that QQ is not wrapped due to not having access to the chinese yuanshen launcher.
|
||||||
|
You can contribute to the project if you want :D
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
- :class:`str` qq_id: The id of the QQ post
|
||||||
|
- :class:`str` name: The name of the QQ post
|
||||||
|
- :class:`int` number: The number of the QQ post
|
||||||
|
- :class:`str` code: The QQ post url.
|
||||||
|
- :class:`dict` raw: The launcher raw information.
|
||||||
|
"""
|
||||||
|
def __init__(self, qq_id, name, number, code, raw):
|
||||||
|
self.qq_id = qq_id
|
||||||
|
self.name = name
|
||||||
|
self.number = number
|
||||||
|
self.code = code
|
||||||
|
self.raw = raw
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(raw: dict) -> 'QQ':
|
||||||
|
"""Creates a QQ object from a dictionary
|
||||||
|
|
||||||
|
Args:
|
||||||
|
raw (dict): The raw dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QQ: The QQ object
|
||||||
|
"""
|
||||||
|
qq_id = raw.get('qq_id')
|
||||||
|
name = raw.get('name')
|
||||||
|
number = int(raw.get('number'))
|
||||||
|
code = raw.get('code')
|
||||||
|
return QQ(qq_id, name, number, code, raw)
|
8
worthless/classes/mhyresponse.py
Normal file
8
worthless/classes/mhyresponse.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class mhyResponse:
|
||||||
|
"""Simple class for wrapping miHoYo web response
|
||||||
|
Currently not used for anything.
|
||||||
|
"""
|
||||||
|
def __init__(self, retcode, message, data):
|
||||||
|
self.retcode = retcode
|
||||||
|
self.message = message
|
||||||
|
self.data = data
|
@ -1,6 +1,11 @@
|
|||||||
LAUNCHER_API_URL = "https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api"
|
APP_NAME="worthless"
|
||||||
|
APP_AUTHOR="tretrauit"
|
||||||
|
LAUNCHER_API_URL_OS = "https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api"
|
||||||
|
LAUNCHER_API_URL_CN = "https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api"
|
||||||
PATCH_GIT_URL = "https://notabug.org/Krock/dawn"
|
PATCH_GIT_URL = "https://notabug.org/Krock/dawn"
|
||||||
TELEMETRY_URL_LIST = [
|
TELEMETRY_URL_LIST = [
|
||||||
"log-upload-os.mihoyo.com",
|
"log-upload-os.mihoyo.com",
|
||||||
|
"log-upload.mihoyo.com",
|
||||||
"overseauspider.yuanshen.com"
|
"overseauspider.yuanshen.com"
|
||||||
|
"uspider.yuanshen.com"
|
||||||
]
|
]
|
||||||
|
@ -4,40 +4,66 @@ import argparse
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def interactive_ui(gamedir=Path.cwd()):
|
class UI:
|
||||||
raise NotImplementedError("Interactive UI is not implemented")
|
def __init__(self, gamedir: str, noconfirm: bool) -> None:
|
||||||
|
self._noconfirm = noconfirm
|
||||||
|
self._gamedir = gamedir
|
||||||
|
|
||||||
|
def _ask(self, title, description):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def update_game(gamedir=Path.cwd(), noconfirm=False):
|
def install_game(self):
|
||||||
|
# TODO
|
||||||
|
raise NotImplementedError("Install game is not implemented.")
|
||||||
|
|
||||||
|
def update_game(self):
|
||||||
print("Checking for current game version...")
|
print("Checking for current game version...")
|
||||||
# Call check_game_version()
|
# Call check_game_version()
|
||||||
print("Updating game...")
|
print("Updating game...")
|
||||||
# Call update_game(fromver)
|
# Call update_game(fromver)
|
||||||
raise NotImplementedError("Update game is not implemented")
|
raise NotImplementedError("Update game is not implemented.")
|
||||||
|
|
||||||
|
def interactive_ui(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(prog="worthless-launcher", description="A worthless launcher written in Python.")
|
parser = argparse.ArgumentParser(prog="worthless", description="A worthless launcher written in Python.")
|
||||||
parser.add_argument("-D", "-d", "--dir", action="store", type=Path, default=Path.cwd(),
|
parser.add_argument("-D", "-d", "--dir", action="store", type=Path, default=Path.cwd(),
|
||||||
help="Specify the game directory (default current working directory)")
|
help="Specify the game directory (default current working directory)")
|
||||||
parser.add_argument("-I", "-i", "--install", action="store_true",
|
parser.add_argument("-S", "--install", action="store_true",
|
||||||
help="Install the game (if not already installed, else do nothing)")
|
help="Install the game (if not already installed, else do nothing)")
|
||||||
parser.add_argument("-U", "-u", "--update", action="store_true", help="Update the game (if not updated)")
|
parser.add_argument("-U", "--install-from-file", action="store_true",
|
||||||
|
help="Install the game from the game archive (if not already installed, \
|
||||||
|
else update from archive)")
|
||||||
|
parser.add_argument("-Sp", "--patch", action="store_true",
|
||||||
|
help="Patch the game (if not already patched, else do nothing)")
|
||||||
|
parser.add_argument("-Sy", "--update", action="store_true",
|
||||||
|
help="Update the game and specified voiceover pack only (or install if not found)")
|
||||||
|
parser.add_argument("-Syu", "--update", action="store_true",
|
||||||
|
help="Update the game and all installed voiceover packs (or install if not found)")
|
||||||
|
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("-Rv", "--remove-voiceover", action="store_true", help="Remove a Voiceover pack (if installed)")
|
||||||
parser.add_argument("--noconfirm", action="store_true",
|
parser.add_argument("--noconfirm", action="store_true",
|
||||||
help="Do not ask any questions. (Ignored in interactive mode)")
|
help="Do not ask any for confirmation. (Ignored in interactive mode)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
print(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
|
||||||
|
ui = UI(args.dir, args.noconfirm)
|
||||||
|
|
||||||
if args.install and args.update:
|
if args.install and args.update:
|
||||||
raise ValueError("Cannot specify both --install and --update arguments.")
|
raise ValueError("Cannot specify both --install and --update arguments.")
|
||||||
|
|
||||||
if args.install:
|
if args.install:
|
||||||
raise NotImplementedError("Install game is not implemented")
|
ui.install_game()
|
||||||
|
|
||||||
if args.update:
|
if args.update:
|
||||||
update_game(args.dir, args.noconfirm)
|
ui.update_game()
|
||||||
return
|
return
|
||||||
|
|
||||||
interactive_ui(args.dir)
|
if interactive_mode:
|
||||||
|
ui.interactive_ui()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
33
worthless/installer.py
Normal file
33
worthless/installer.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import asyncio
|
||||||
|
import tarfile
|
||||||
|
import constants
|
||||||
|
import appdirs
|
||||||
|
import aiofiles
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import aiohttp
|
||||||
|
from worthless.launcher import Launcher
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
|
||||||
|
class Installer:
|
||||||
|
def __init__(self, gamedir: str | Path = Path.cwd(), overseas: bool = True):
|
||||||
|
if isinstance(gamedir, str):
|
||||||
|
gamedir = Path(gamedir)
|
||||||
|
self._gamedir = gamedir
|
||||||
|
config_file = self._gamedir.joinpath("config.ini")
|
||||||
|
self._config_file = config_file.resolve()
|
||||||
|
self._version = None
|
||||||
|
self._overseas = overseas
|
||||||
|
self._launcher = Launcher(self._gamedir, self._overseas)
|
||||||
|
if config_file.exists():
|
||||||
|
self._version = self._read_version_from_config()
|
||||||
|
else: # TODO: Use An Anime Game Launcher method (which is more brutal, but it works)
|
||||||
|
self._version = "mangosus"
|
||||||
|
|
||||||
|
def _read_version_from_config(self):
|
||||||
|
if self._config_file.exists():
|
||||||
|
raise FileNotFoundError(f"Config file {self._config_file} not found")
|
||||||
|
cfg = ConfigParser()
|
||||||
|
cfg.read(str(self._config_file))
|
||||||
|
return cfg.get("miHoYo", "game_version")
|
@ -10,14 +10,28 @@ class Launcher:
|
|||||||
Contains functions to get information from server and client like the official launcher.
|
Contains functions to get information from server and client like the official launcher.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, gamedir=Path.cwd()):
|
def __init__(self, gamedir=Path.cwd(), language=None, overseas=True):
|
||||||
"""Initialize the launcher API
|
"""Initialize the launcher API
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
gamedir (Path): Path to the game directory.
|
gamedir (Path): Path to the game directory.
|
||||||
"""
|
"""
|
||||||
self._api = constants.LAUNCHER_API_URL
|
self._overseas = overseas
|
||||||
self._lang = self._get_system_language()
|
if overseas:
|
||||||
|
self._api = constants.LAUNCHER_API_URL_OS
|
||||||
|
self._params = {
|
||||||
|
"key": "gcStgarh",
|
||||||
|
"launcher_id": "10",
|
||||||
|
}
|
||||||
|
self._lang = self._get_system_language() if not language else language.lower().replace("_", "-")
|
||||||
|
else:
|
||||||
|
self._api = constants.LAUNCHER_API_URL_CN
|
||||||
|
self._params = {
|
||||||
|
"key": "eYd89JmJ",
|
||||||
|
"launcher_id": "18",
|
||||||
|
"channel_id": "1"
|
||||||
|
}
|
||||||
|
self._lang = "zh-cn" # Use chinese language because this is Pooh version
|
||||||
if isinstance(gamedir, str):
|
if isinstance(gamedir, str):
|
||||||
gamedir = Path(gamedir)
|
gamedir = Path(gamedir)
|
||||||
self._gamedir = gamedir.resolve()
|
self._gamedir = gamedir.resolve()
|
||||||
@ -36,18 +50,6 @@ class Launcher:
|
|||||||
request_info=rsp.request_info)
|
request_info=rsp.request_info)
|
||||||
return rsp_json
|
return rsp_json
|
||||||
|
|
||||||
async def _get_launcher_info(self, adv=True) -> launcher.Info:
|
|
||||||
params = {"key": "gcStgarh",
|
|
||||||
"filter_adv": str(adv).lower(),
|
|
||||||
"launcher_id": "10",
|
|
||||||
"language": self._lang}
|
|
||||||
rsp = await self._get(self._api + "/content", params=params)
|
|
||||||
if rsp["data"]["adv"] is None:
|
|
||||||
params["language"] = "en-us"
|
|
||||||
rsp = await self._get(self._api + "/content", params=params)
|
|
||||||
lc_info = launcher.Info.from_dict(rsp["data"])
|
|
||||||
return lc_info
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_system_language() -> str:
|
def _get_system_language() -> str:
|
||||||
"""Gets system language compatible with server parameters.
|
"""Gets system language compatible with server parameters.
|
||||||
@ -61,16 +63,27 @@ class Launcher:
|
|||||||
lowercase_lang = lang.lower().replace("_", "-")
|
lowercase_lang = lang.lower().replace("_", "-")
|
||||||
return lowercase_lang
|
return lowercase_lang
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return "en-us"
|
return "en-us" # Fallback to English if locale is not supported
|
||||||
|
|
||||||
async def override_gamedir(self, gamedir: str) -> None:
|
async def _get_launcher_info(self, adv=True) -> launcher.Info:
|
||||||
|
params = self._params | {"filter_adv": str(adv).lower(),
|
||||||
|
"language": self._lang}
|
||||||
|
rsp = await self._get(self._api + "/content", params=params)
|
||||||
|
if rsp["data"]["adv"] is None:
|
||||||
|
params["language"] = "en-us"
|
||||||
|
rsp = await self._get(self._api + "/content", params=params)
|
||||||
|
lc_info = launcher.Info.from_dict(rsp["data"])
|
||||||
|
return lc_info
|
||||||
|
|
||||||
|
async def override_gamedir(self, gamedir: str | Path) -> None:
|
||||||
"""Overrides game directory with another directory.
|
"""Overrides game directory with another directory.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
gamedir (str): New directory to override with.
|
gamedir (str): New directory to override with.
|
||||||
"""
|
"""
|
||||||
|
if isinstance(gamedir, str):
|
||||||
self._gamedir = Path(gamedir).resolve()
|
gamedir = Path(gamedir).resolve()
|
||||||
|
self._gamedir = gamedir
|
||||||
|
|
||||||
async def override_language(self, language: str) -> None:
|
async def override_language(self, language: str) -> None:
|
||||||
"""Overrides system detected language with another language.
|
"""Overrides system detected language with another language.
|
||||||
@ -92,8 +105,7 @@ class Launcher:
|
|||||||
aiohttp.ClientResponseError: An error occurred while fetching the information.
|
aiohttp.ClientResponseError: An error occurred while fetching the information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
rsp = await self._get(self._api + "/resource", params={"key": "gcStgarh",
|
rsp = await self._get(self._api + "/resource", params=self._params)
|
||||||
"launcher_id": "10"})
|
|
||||||
return rsp
|
return rsp
|
||||||
|
|
||||||
async def get_launcher_info(self) -> launcher.Info:
|
async def get_launcher_info(self) -> launcher.Info:
|
||||||
|
@ -1,31 +1,96 @@
|
|||||||
|
import asyncio
|
||||||
|
import tarfile
|
||||||
import constants
|
import constants
|
||||||
|
import appdirs
|
||||||
|
import aiofiles
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
class Patcher:
|
class Patcher:
|
||||||
def __init__(self, gamedir=Path.cwd()):
|
def __init__(self, gamedir=Path.cwd(), data_dir: str | Path = None, patch_url: str = None):
|
||||||
self._gamedir = gamedir
|
self._gamedir = gamedir
|
||||||
self._patch_url = constants.PATCH_GIT_URL
|
self._patch_url = (patch_url if patch_url else constants.PATCH_GIT_URL).replace('http://', 'https://')
|
||||||
|
if not data_dir:
|
||||||
|
self._appdirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
|
||||||
|
self._patch_path = Path(self._appdirs.user_data_dir).joinpath("Patch")
|
||||||
|
self._temp_path = Path(self._appdirs.user_cache_dir)
|
||||||
|
else:
|
||||||
|
if not isinstance(data_dir, Path):
|
||||||
|
override_data_dir = Path(data_dir)
|
||||||
|
self._patch_path = data_dir.joinpath("Patch")
|
||||||
|
self._temp_path = data_dir.joinpath("Temp")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _get(url, **kwargs) -> aiohttp.ClientResponse:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
rsp = await session.get(url, **kwargs)
|
||||||
|
rsp.raise_for_status()
|
||||||
|
return rsp
|
||||||
|
|
||||||
|
async def _get_git_archive(self, archive_format="tar.gz", branch="master"):
|
||||||
|
"""
|
||||||
|
Get the git archive of the patch repository.
|
||||||
|
This supports Gitea API and also introduce workaround for https://notabug.org
|
||||||
|
|
||||||
|
:return: Archive file in bytes
|
||||||
|
"""
|
||||||
|
# Replace http with https
|
||||||
|
if self._patch_url.startswith('https://notabug.org'):
|
||||||
|
archive_url = self._patch_url + '/archive/master.{}'.format(archive_format)
|
||||||
|
return await (await self._get(archive_url)).read()
|
||||||
|
try:
|
||||||
|
url_split = self._patch_url.split('//')
|
||||||
|
git_server = url_split[0]
|
||||||
|
git_owner, git_repo = url_split[1].split('/')
|
||||||
|
archive_url = git_server + '/api/v1/repos/{}/{}/archive/{}.{}'.format(
|
||||||
|
git_owner, git_repo, branch, archive_format
|
||||||
|
)
|
||||||
|
archive = await self._get(archive_url)
|
||||||
|
except aiohttp.ClientResponseError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return await archive.read()
|
||||||
|
return
|
||||||
|
|
||||||
|
async def _download_repo(self):
|
||||||
|
if shutil.which("git"):
|
||||||
|
if not self._patch_path.exists() or not self._patch_path.is_dir() \
|
||||||
|
or not self._patch_path.joinpath(".git").exists():
|
||||||
|
await asyncio.create_subprocess_exec("git", "clone", self._patch_url, str(self._patch_path))
|
||||||
|
else:
|
||||||
|
await asyncio.create_subprocess_exec("git", "pull", cwd=str(self._patch_path))
|
||||||
|
else:
|
||||||
|
archive = await self._get_git_archive()
|
||||||
|
if not archive:
|
||||||
|
raise RuntimeError("Cannot download patch repository")
|
||||||
|
|
||||||
|
with tarfile.open(archive) as tar:
|
||||||
|
tar.extractall(self._patch_path)
|
||||||
|
|
||||||
def override_patch_url(self, url) -> None:
|
def override_patch_url(self, url) -> None:
|
||||||
"""
|
"""
|
||||||
Override the patch url.
|
Override the patch url.
|
||||||
|
|
||||||
:param url: Patch repository url, the url must be a valid git repository.
|
:param url: Patch repository url, the url must be a valid git repository.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._patch_url = url
|
self._patch_url = url
|
||||||
|
|
||||||
def download_patch(self) -> None:
|
async def download_patch(self) -> None:
|
||||||
"""
|
"""
|
||||||
If `git` exists, this will clone the patch git url and save it to a temporary directory.
|
If `git` exists, this will clone the patch git url and save it to a temporary directory.
|
||||||
Else, this will download the patch from the patch url and save it to a temporary directory. (Not reliable)
|
Else, this will download the patch from the patch url and save it to a temporary directory. (Not reliable)
|
||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
pass
|
await self._download_repo()
|
||||||
|
|
||||||
def apply_patch(self, crash_fix=False) -> None:
|
def apply_patch(self, crash_fix=False) -> None:
|
||||||
"""
|
"""
|
||||||
Patch the game (and optionally patch the login door crash fix if specified)
|
Patch the game (and optionally patch the login door crash fix if specified)
|
||||||
|
|
||||||
:param crash_fix: Whether to patch the login door crash fix or not
|
:param crash_fix: Whether to patch the login door crash fix or not
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
@ -34,6 +99,7 @@ class Patcher:
|
|||||||
def revert_patch(self):
|
def revert_patch(self):
|
||||||
"""
|
"""
|
||||||
Revert the patch (and revert the login door crash fix if patched)
|
Revert the patch (and revert the login door crash fix if patched)
|
||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
Loading…
Reference in New Issue
Block a user