fix: migrate to HoYoPlay
For now, only update game and voicepack are working
This commit also fixes a couple of bugs too. Tbf I enjoyed HoYoPlay until I don't have enough space to update my HSR so yeah 💀
This commit is contained in:
parent
08c51d2fd8
commit
8ff2a388d7
@ -1,8 +1,10 @@
|
||||
import traceback
|
||||
from cleo.commands.command import Command
|
||||
from cleo.helpers import option, argument
|
||||
from copy import deepcopy
|
||||
from pathlib import PurePath
|
||||
from platform import system
|
||||
from vollerei.hsr.launcher.enums import GameChannel
|
||||
from vollerei.common.enums import GameChannel
|
||||
from vollerei.cli import utils
|
||||
from vollerei.exceptions.game import GameError
|
||||
from vollerei.hsr import Game, Patcher
|
||||
@ -434,11 +436,12 @@ class UpdateCommand(Command):
|
||||
update_diff = State.game.get_update(pre_download=pre_download)
|
||||
game_info = State.game.get_remote_game(pre_download=pre_download)
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
progress.finish(
|
||||
f"<error>Update checking failed with following error: {e} ({e.__context__})</error>"
|
||||
)
|
||||
return
|
||||
if update_diff is None:
|
||||
if update_diff is None or isinstance(game_info.major, str | None):
|
||||
progress.finish("<comment>Game is already updated.</comment>")
|
||||
return
|
||||
progress.finish("<comment>Update available.</comment>")
|
||||
@ -446,16 +449,17 @@ class UpdateCommand(Command):
|
||||
f"The current version is: <comment>{State.game.get_version_str()}</comment>"
|
||||
)
|
||||
self.line(
|
||||
f"The latest version is: <comment>{game_info.latest.version}</comment>"
|
||||
f"The latest version is: <comment>{game_info.major.version}</comment>"
|
||||
)
|
||||
if not self.confirm("Do you want to update the game?"):
|
||||
self.line("<error>Update aborted.</error>")
|
||||
return
|
||||
self.line("Downloading update package...")
|
||||
out_path = State.game.cache.joinpath(update_diff.name)
|
||||
update_game_url = update_diff.game_pkgs[0].url
|
||||
out_path = State.game.cache.joinpath(PurePath(update_game_url).name)
|
||||
try:
|
||||
download_result = utils.download(
|
||||
update_diff.path, out_path, file_len=update_diff.size
|
||||
update_game_url, out_path, file_len=update_diff.game_pkgs[0].size
|
||||
)
|
||||
except Exception as e:
|
||||
self.line_error(f"<error>Couldn't download update: {e}</error>")
|
||||
@ -478,14 +482,14 @@ class UpdateCommand(Command):
|
||||
# Get installed voicepacks
|
||||
installed_voicepacks = State.game.get_installed_voicepacks()
|
||||
# Voicepack update
|
||||
for remote_voicepack in update_diff.voice_packs:
|
||||
for remote_voicepack in update_diff.audio_pkgs:
|
||||
if remote_voicepack.language not in installed_voicepacks:
|
||||
continue
|
||||
# Voicepack is installed, update it
|
||||
archive_file = State.game.cache.joinpath(remote_voicepack.name)
|
||||
archive_file = State.game.cache.joinpath(PurePath(remote_voicepack.url).name)
|
||||
try:
|
||||
download_result = utils.download(
|
||||
remote_voicepack.path, archive_file, file_len=update_diff.size
|
||||
remote_voicepack.url, archive_file, file_len=remote_voicepack.size
|
||||
)
|
||||
except Exception as e:
|
||||
self.line_error(f"<error>Couldn't download update: {e}</error>")
|
||||
|
@ -1,4 +1,35 @@
|
||||
from vollerei.common.api.resource import Resource
|
||||
import requests
|
||||
from vollerei.common.api import resource
|
||||
from vollerei.common.enums import GameChannel
|
||||
from vollerei.constants import LAUNCHER_API
|
||||
|
||||
|
||||
__all__ = ["Resource"]
|
||||
__all__ = ["GamePackage"]
|
||||
|
||||
|
||||
def get_game_packages(
|
||||
channel: GameChannel = GameChannel.Overseas,
|
||||
) -> list[resource.GameInfo]:
|
||||
"""
|
||||
Get game packages information from the launcher API.
|
||||
|
||||
Default channel is overseas.
|
||||
|
||||
Args:
|
||||
channel: Game channel to get the resource information from.
|
||||
|
||||
Returns:
|
||||
Resource: Game resource information.
|
||||
"""
|
||||
resource_path: dict = None
|
||||
match channel:
|
||||
case GameChannel.Overseas:
|
||||
resource_path = LAUNCHER_API.OS
|
||||
case GameChannel.China:
|
||||
resource_path = LAUNCHER_API.CN
|
||||
return resource.from_dict(
|
||||
requests.get(
|
||||
resource_path["url"] + LAUNCHER_API.RESOURCE_PATH,
|
||||
params=resource_path["params"],
|
||||
).json()["data"]
|
||||
)
|
||||
|
@ -1,409 +1,134 @@
|
||||
"""
|
||||
Class wrapper for API endpoint /resource
|
||||
"""
|
||||
|
||||
from vollerei.common.enums import VoicePackLanguage
|
||||
|
||||
|
||||
class Segment:
|
||||
"""
|
||||
A segment of the game archive.
|
||||
|
||||
Attributes:
|
||||
path (str): Segment download path.
|
||||
md5 (str): Segment md5 checksum.
|
||||
package_size (int | None): Segment package size.
|
||||
"""
|
||||
|
||||
path: str
|
||||
md5: str
|
||||
# str -> int and checked if int is 0 then None
|
||||
package_size: int | None
|
||||
|
||||
def __init__(self, path: str, md5: str, package_size: int | None) -> None:
|
||||
self.path = path
|
||||
self.md5 = md5
|
||||
self.package_size = package_size
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Segment":
|
||||
return Segment(
|
||||
data["path"],
|
||||
data["md5"],
|
||||
(
|
||||
int(data["package_size"])
|
||||
if data["package_size"] and data["package_size"] != "0"
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class VoicePack:
|
||||
"""
|
||||
Voice pack information
|
||||
|
||||
`name` maybe converted from `path` if the server returns empty string.
|
||||
|
||||
Attributes:
|
||||
language (VoicePackLanguage): Language of the voice pack.
|
||||
name (str): Voice pack archive name.
|
||||
path (str): Voice pack download path.
|
||||
size (int): Voice pack size.
|
||||
md5 (str): Voice pack md5 checksum.
|
||||
package_size (int): Voice pack package size.
|
||||
"""
|
||||
|
||||
language: VoicePackLanguage
|
||||
name: str
|
||||
path: str
|
||||
# str -> int
|
||||
size: int
|
||||
md5: str
|
||||
# str -> int
|
||||
package_size: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
language: VoicePackLanguage,
|
||||
name: str,
|
||||
path: str,
|
||||
size: int,
|
||||
md5: str,
|
||||
package_size: int,
|
||||
) -> None:
|
||||
self.language = language
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.size = size
|
||||
self.md5 = md5
|
||||
self.package_size = package_size
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "VoicePack":
|
||||
return VoicePack(
|
||||
VoicePackLanguage.from_remote_str(data["language"]),
|
||||
data["name"],
|
||||
data["path"],
|
||||
int(data["size"]),
|
||||
data["md5"],
|
||||
int(data["package_size"]),
|
||||
)
|
||||
|
||||
|
||||
class Diff:
|
||||
"""
|
||||
Game resource diff from a version to latest information
|
||||
|
||||
Attributes:
|
||||
TODO
|
||||
"""
|
||||
|
||||
name: str
|
||||
version: str
|
||||
path: str
|
||||
# str -> int
|
||||
size: int
|
||||
md5: str
|
||||
is_recommended_update: bool
|
||||
voice_packs: list[VoicePack]
|
||||
# str -> int
|
||||
package_size: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
version: str,
|
||||
path: str,
|
||||
size: int,
|
||||
md5: str,
|
||||
is_recommended_update: bool,
|
||||
voice_packs: list[VoicePack],
|
||||
package_size: int,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.path = path
|
||||
self.size = size
|
||||
self.md5 = md5
|
||||
self.is_recommended_update = is_recommended_update
|
||||
self.voice_packs = voice_packs
|
||||
self.package_size = package_size
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Diff":
|
||||
return Diff(
|
||||
data["name"],
|
||||
data["version"],
|
||||
data["path"],
|
||||
int(data["size"]),
|
||||
data["md5"],
|
||||
data["is_recommended_update"],
|
||||
[VoicePack.from_dict(i) for i in data["voice_packs"]],
|
||||
int(data["package_size"]),
|
||||
)
|
||||
|
||||
|
||||
class Latest:
|
||||
"""
|
||||
Latest game resource information
|
||||
|
||||
`name` maybe converted from `path` if the server returns empty string,
|
||||
and if `path` is empty too then it'll convert the name from the first
|
||||
segment of `segments` list.
|
||||
|
||||
`path` maybe None if the server returns empty string, in that case
|
||||
you'll have to download the game using `segments` list and merge them.
|
||||
|
||||
`voice_packs` will be empty for Star Rail, they force you to download
|
||||
in-game instead.
|
||||
|
||||
`decompressed_path` is useful for repairing game files by only having
|
||||
to re-download the corrupted files.
|
||||
|
||||
`segments` is a list of game archive segments, you'll have to download
|
||||
them and merge them together to get the full game archive. Not available
|
||||
on Star Rail.
|
||||
|
||||
Attributes:
|
||||
name (str): Game archive name.
|
||||
version (str): Game version in the archive.
|
||||
path (str | None): Game archive download path.
|
||||
size (int): Game archive size in bytes.
|
||||
md5 (str): Game archive MD5 checksum.
|
||||
entry (str): Game entry file (e.g. GenshinImpact.exe).
|
||||
voice_packs (list[VoicePack]): Game voice packs.
|
||||
decompressed_path (str | None): Game archive decompressed path.
|
||||
segments (list[Segment]): Game archive segments.
|
||||
package_size (int): Game archive package size in bytes.
|
||||
"""
|
||||
|
||||
name: str
|
||||
version: str
|
||||
path: str | None
|
||||
# str -> int
|
||||
size: int
|
||||
md5: str
|
||||
entry: str
|
||||
voice_packs: list[VoicePack]
|
||||
# str but checked for empty string
|
||||
decompressed_path: str | None
|
||||
segments: list[Segment]
|
||||
# str -> int
|
||||
package_size: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
version: str,
|
||||
path: str,
|
||||
size: int,
|
||||
md5: str,
|
||||
entry: str,
|
||||
voice_packs: list[VoicePack],
|
||||
decompressed_path: str | None,
|
||||
segments: list[Segment],
|
||||
package_size: int,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.path = path
|
||||
self.size = size
|
||||
self.md5 = md5
|
||||
self.entry = entry
|
||||
self.voice_packs = voice_packs
|
||||
self.decompressed_path = decompressed_path
|
||||
self.segments = segments
|
||||
self.package_size = package_size
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Latest":
|
||||
if data["name"] == "":
|
||||
if data["path"] == "":
|
||||
data["name"] = data["segments"][0]["path"].split("/")[-1]
|
||||
else:
|
||||
data["name"] = data["path"].split("/")[-1]
|
||||
return Latest(
|
||||
data["name"],
|
||||
data["version"],
|
||||
data["path"] if data["path"] != "" else None,
|
||||
int(data["size"]),
|
||||
data["md5"],
|
||||
data["entry"],
|
||||
[VoicePack.from_dict(i) for i in data["voice_packs"]],
|
||||
data["decompressed_path"] if data["decompressed_path"] != "" else None,
|
||||
[Segment.from_dict(i) for i in data["segments"]],
|
||||
int(data["package_size"]),
|
||||
)
|
||||
|
||||
|
||||
class Game:
|
||||
latest: Latest
|
||||
diffs: list[Diff]
|
||||
|
||||
def __init__(self, latest: Latest, diffs: list[Diff]) -> None:
|
||||
self.latest = latest
|
||||
self.diffs = diffs
|
||||
def __init__(self, id: str, biz: str):
|
||||
self.id = id
|
||||
self.biz = biz
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Game":
|
||||
return Game(
|
||||
Latest.from_dict(data["latest"]), [Diff.from_dict(i) for i in data["diffs"]]
|
||||
)
|
||||
return Game(id=data["id"], biz=data["biz"])
|
||||
|
||||
|
||||
class Plugin:
|
||||
name: str
|
||||
# str but checked for empty string
|
||||
version: str | None
|
||||
path: str
|
||||
# str -> int
|
||||
size: int
|
||||
md5: str
|
||||
# str but checked for empty string
|
||||
entry: str | None
|
||||
# str -> int
|
||||
package_size: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
version: str | None,
|
||||
path: str,
|
||||
size: int,
|
||||
md5: str,
|
||||
entry: str | None,
|
||||
package_size: int,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.path = path
|
||||
class GamePackage:
|
||||
def __init__(self, url: str, md5: str, size: int, decompressed_size: int):
|
||||
self.url = url
|
||||
self.md5 = md5
|
||||
self.size = size
|
||||
self.md5 = md5
|
||||
self.entry = entry
|
||||
self.package_size = package_size
|
||||
self.decompressed_size = decompressed_size
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Plugin":
|
||||
return Plugin(
|
||||
data["name"],
|
||||
data["version"] if data["version"] != "" else None,
|
||||
data["path"],
|
||||
int(data["size"]),
|
||||
data["md5"],
|
||||
data["entry"] if data["entry"] != "" else None,
|
||||
int(data["package_size"]),
|
||||
def from_dict(data: dict) -> "GamePackage":
|
||||
return GamePackage(
|
||||
url=data["url"],
|
||||
md5=data["md5"],
|
||||
size=int(data["size"]),
|
||||
decompressed_size=int(data["decompressed_size"]),
|
||||
)
|
||||
|
||||
|
||||
class LauncherPlugin:
|
||||
plugins: list[Plugin]
|
||||
# str -> int
|
||||
version: int
|
||||
|
||||
def __init__(self, plugins: list[Plugin], version: int) -> None:
|
||||
self.plugins = plugins
|
||||
self.version = version
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "LauncherPlugin":
|
||||
return LauncherPlugin(
|
||||
[Plugin.from_dict(i) for i in data["plugins"]], int(data["version"])
|
||||
)
|
||||
|
||||
|
||||
class DeprecatedPackage:
|
||||
name: str
|
||||
md5: str
|
||||
|
||||
def __init__(self, name: str, md5: str) -> None:
|
||||
self.name = name
|
||||
self.md5 = md5
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "DeprecatedPackage":
|
||||
return DeprecatedPackage(data["name"], data["md5"])
|
||||
|
||||
|
||||
class DeprecatedFile:
|
||||
path: str
|
||||
# str but checked for empty string
|
||||
md5: str | None
|
||||
|
||||
def __init__(self, path: str, md5: str | None) -> None:
|
||||
self.path = path
|
||||
self.md5 = md5
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "DeprecatedFile":
|
||||
return DeprecatedFile(data["path"], data["md5"] if data["md5"] != "" else None)
|
||||
|
||||
|
||||
class Resource:
|
||||
"""
|
||||
Data class for /resource endpoint
|
||||
|
||||
I'm still unclear about `force_update` and `sdk` attributes, so I'll
|
||||
leave them as None for now.
|
||||
|
||||
Attributes:
|
||||
game (Game): Game resource information.
|
||||
plugin (LauncherPlugin): Launcher plugin information.
|
||||
web_url (str): Game official launcher web URL.
|
||||
force_update (None): Not used by official launcher I guess?
|
||||
pre_download_game (Game | None): Pre-download game resource information.
|
||||
deprecated_packages (list[DeprecatedPackage]): Deprecated game packages.
|
||||
sdk (None): Maybe for Bilibili version of Genshin?
|
||||
deprecated_files (list[DeprecatedFile]): Deprecated game files.
|
||||
"""
|
||||
|
||||
# I'm generous enough to convert the string into int
|
||||
# for you guys, wtf Mihoyo?
|
||||
game: Game
|
||||
# ?? Mihoyo for plugin["plugins"] which is a list of Plugin objects
|
||||
plugin: LauncherPlugin
|
||||
web_url: str
|
||||
# ?? Mihoyo
|
||||
force_update: None
|
||||
# Will be a Game object if a pre-download is available.
|
||||
pre_download_game: Game | None
|
||||
deprecated_packages: list[DeprecatedPackage]
|
||||
# Maybe a SDK for Bilibili version in Genshin?
|
||||
sdk: None
|
||||
deprecated_files: list[DeprecatedFile]
|
||||
|
||||
class AudioPackage:
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
plugin: Plugin,
|
||||
web_url: str,
|
||||
force_update: None,
|
||||
pre_download_game: Game | None,
|
||||
deprecated_packages: list[DeprecatedPackage],
|
||||
sdk: None,
|
||||
deprecated_files: list[DeprecatedFile],
|
||||
) -> None:
|
||||
self.game = game
|
||||
self.plugin = plugin
|
||||
self.web_url = web_url
|
||||
self.force_update = force_update
|
||||
self.pre_download_game = pre_download_game
|
||||
self.deprecated_packages = deprecated_packages
|
||||
self.sdk = sdk
|
||||
self.deprecated_files = deprecated_files
|
||||
language: VoicePackLanguage,
|
||||
url: str,
|
||||
md5: str,
|
||||
size: int,
|
||||
decompressed_size: int,
|
||||
):
|
||||
self.language = language
|
||||
self.url = url
|
||||
self.md5 = md5
|
||||
self.size = size
|
||||
self.decompressed_size = decompressed_size
|
||||
|
||||
@staticmethod
|
||||
def from_dict(json: dict) -> "Resource":
|
||||
return Resource(
|
||||
Game.from_dict(json["game"]),
|
||||
LauncherPlugin.from_dict(json["plugin"]),
|
||||
json["web_url"],
|
||||
json["force_update"],
|
||||
(
|
||||
Game.from_dict(json["pre_download_game"])
|
||||
if json["pre_download_game"]
|
||||
else None
|
||||
),
|
||||
[DeprecatedPackage.from_dict(x) for x in json["deprecated_packages"]],
|
||||
json["sdk"],
|
||||
[DeprecatedFile.from_dict(x) for x in json["deprecated_files"]],
|
||||
def from_dict(data: dict) -> "AudioPackage":
|
||||
return AudioPackage(
|
||||
language=VoicePackLanguage.from_remote_str(data["language"]),
|
||||
url=data["url"],
|
||||
md5=data["md5"],
|
||||
size=int(data["size"]),
|
||||
decompressed_size=int(data["decompressed_size"]),
|
||||
)
|
||||
|
||||
|
||||
class Major:
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
game_pkgs: list[GamePackage],
|
||||
audio_pkgs: list[AudioPackage],
|
||||
res_list_url: str,
|
||||
):
|
||||
self.version = version
|
||||
self.game_pkgs = game_pkgs
|
||||
self.audio_pkgs = audio_pkgs
|
||||
self.res_list_url = res_list_url
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Major":
|
||||
return Major(
|
||||
version=data["version"],
|
||||
game_pkgs=[GamePackage(**x) for x in data["game_pkgs"]],
|
||||
audio_pkgs=[AudioPackage(**x) for x in data["audio_pkgs"]],
|
||||
res_list_url=data["res_list_url"],
|
||||
)
|
||||
|
||||
|
||||
# Currently patch has the same fields as major
|
||||
Patch = Major
|
||||
|
||||
|
||||
class Main:
|
||||
def __init__(self, major: Major, patches: list[Patch]):
|
||||
self.major = major
|
||||
self.patches = patches
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Main":
|
||||
return Main(
|
||||
major=Major.from_dict(data["major"]),
|
||||
patches=[Patch.from_dict(x) for x in data["patches"]],
|
||||
)
|
||||
|
||||
|
||||
class PreDownload:
|
||||
def __init__(self, major: Major | str | None, patches: list[Patch]):
|
||||
self.major = major
|
||||
self.patches = patches
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "PreDownload":
|
||||
return PreDownload(
|
||||
major=(
|
||||
data["major"]
|
||||
if isinstance(data["major"], str | None)
|
||||
else Major.from_dict(data["major"])
|
||||
),
|
||||
patches=[Patch.from_dict(x) for x in data["patches"]],
|
||||
)
|
||||
|
||||
|
||||
# Why miHoYo uses the same name "game_packages" for this big field and smol field
|
||||
class GameInfo:
|
||||
def __init__(self, game: Game, main: Main, pre_download: PreDownload):
|
||||
self.game = game
|
||||
self.main = main
|
||||
self.pre_download = pre_download
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "GameInfo":
|
||||
return GameInfo(
|
||||
game=Game.from_dict(data["game"]),
|
||||
main=Main.from_dict(data["main"]),
|
||||
pre_download=PreDownload.from_dict(data["pre_download"]),
|
||||
)
|
||||
|
||||
|
||||
def from_dict(data: dict) -> list[GameInfo]:
|
||||
game_pkgs = []
|
||||
for pkg in data["game_packages"]:
|
||||
game_pkgs.append(GameInfo.from_dict(pkg))
|
||||
return game_pkgs
|
||||
|
@ -1,6 +1,11 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class GameChannel(Enum):
|
||||
Overseas = 0
|
||||
China = 1
|
||||
|
||||
|
||||
class VoicePackLanguage(Enum):
|
||||
Japanese = "ja-jp"
|
||||
Chinese = "zh-cn"
|
||||
|
@ -1,3 +1,21 @@
|
||||
class LAUNCHER_API:
|
||||
"""Launcher API constants."""
|
||||
|
||||
RESOURCE_PATH: str = "hyp/hyp-connect/api/getGamePackages"
|
||||
OS: dict = {
|
||||
"url": "https://sg-hyp-api.hoyoverse.com/",
|
||||
"params": {
|
||||
"launcher_id": "VYTpXlbWo8",
|
||||
},
|
||||
}
|
||||
CN: dict = {
|
||||
"url": "https://hyp-api.mihoyo.com/",
|
||||
"params": {
|
||||
"launcher_id": "jGHBHlcOq1",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
TELEMETRY_HOSTS = [
|
||||
# Global
|
||||
"log-upload-os.hoyoverse.com",
|
||||
|
@ -1,24 +0,0 @@
|
||||
class LAUNCHER_API:
|
||||
"""Launcher API constants."""
|
||||
|
||||
RESOURCE_PATH: str = "mdk/launcher/api/resource"
|
||||
OS: dict = {
|
||||
"url": "https://hkrpg-launcher-static.hoyoverse.com/hkrpg_global/",
|
||||
"params": {
|
||||
"channel_id": 1,
|
||||
"key": "vplOVX8Vn7cwG8yb",
|
||||
"launcher_id": 35,
|
||||
},
|
||||
}
|
||||
ASIA: dict = {}
|
||||
CN: dict = {
|
||||
"url": "https://api-launcher.mihoyo.com/hkrpg_cn/mdk/launcher/api/resource",
|
||||
"params": {
|
||||
"channel_id": 1,
|
||||
"key": "6KcVuOkbcqjJomjZ",
|
||||
"launcher_id": 33,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
LATEST_VERSION = (7, 2, 0)
|
@ -1,9 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class GameChannel(Enum):
|
||||
Global = 0
|
||||
Asia = 1
|
||||
Taiwan = 2
|
||||
Korea = 3
|
||||
China = 4
|
@ -1,26 +1,4 @@
|
||||
class LAUNCHER_API:
|
||||
"""Launcher API constants."""
|
||||
|
||||
RESOURCE_PATH: str = "mdk/launcher/api/resource"
|
||||
OS: dict = {
|
||||
"url": "https://hkrpg-launcher-static.hoyoverse.com/hkrpg_global/",
|
||||
"params": {
|
||||
"channel_id": 1,
|
||||
"key": "vplOVX8Vn7cwG8yb",
|
||||
"launcher_id": 35,
|
||||
},
|
||||
}
|
||||
CN: dict = {
|
||||
"url": "https://api-launcher.mihoyo.com/hkrpg_cn/mdk/launcher/api/resource",
|
||||
"params": {
|
||||
"channel_id": 1,
|
||||
"key": "6KcVuOkbcqjJomjZ",
|
||||
"launcher_id": 33,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
LATEST_VERSION = (1, 6, 0)
|
||||
LATEST_VERSION = (2, 5, 0)
|
||||
MD5SUMS = {
|
||||
"1.0.5": {
|
||||
"cn": {
|
||||
|
@ -1,13 +1,10 @@
|
||||
import requests
|
||||
|
||||
from vollerei.common.api import Resource
|
||||
from vollerei.hsr.constants import LAUNCHER_API
|
||||
from vollerei.hsr.launcher.enums import GameChannel
|
||||
from vollerei.common.api import get_game_packages, resource
|
||||
from vollerei.common.enums import GameChannel
|
||||
|
||||
|
||||
def get_resource(channel: GameChannel = GameChannel.Overseas) -> Resource:
|
||||
def get_game_package(channel: GameChannel = GameChannel.Overseas) -> resource.GameInfo:
|
||||
"""
|
||||
Get game resource information from the launcher API.
|
||||
Get game package information from the launcher API.
|
||||
|
||||
Default channel is overseas.
|
||||
|
||||
@ -17,15 +14,7 @@ def get_resource(channel: GameChannel = GameChannel.Overseas) -> Resource:
|
||||
Returns:
|
||||
Resource: Game resource information.
|
||||
"""
|
||||
resource_path: dict = None
|
||||
match channel:
|
||||
case GameChannel.Overseas:
|
||||
resource_path = LAUNCHER_API.OS
|
||||
case GameChannel.China:
|
||||
resource_path = LAUNCHER_API.CN
|
||||
return Resource.from_dict(
|
||||
requests.get(
|
||||
resource_path["url"] + LAUNCHER_API.RESOURCE_PATH,
|
||||
params=resource_path["params"],
|
||||
).json()["data"]
|
||||
)
|
||||
game_packages = get_game_packages(channel=channel)
|
||||
for package in game_packages:
|
||||
if "hkrpg" in package.game.biz:
|
||||
return package
|
||||
|
@ -1,6 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class GameChannel(Enum):
|
||||
Overseas = 0
|
||||
China = 1
|
@ -3,12 +3,12 @@ from configparser import ConfigParser
|
||||
from hashlib import md5
|
||||
from io import IOBase
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePath
|
||||
from shutil import move, copyfile
|
||||
from vollerei.abc.launcher.game import GameABC
|
||||
from vollerei.common import ConfigFile, functions
|
||||
from vollerei.common.api import resource
|
||||
from vollerei.common.enums import VoicePackLanguage
|
||||
from vollerei.common.enums import VoicePackLanguage, GameChannel
|
||||
from vollerei.exceptions.game import (
|
||||
GameAlreadyUpdatedError,
|
||||
GameNotInstalledError,
|
||||
@ -16,7 +16,6 @@ from vollerei.exceptions.game import (
|
||||
ScatteredFilesNotAvailableError,
|
||||
)
|
||||
from vollerei.hsr.constants import MD5SUMS
|
||||
from vollerei.hsr.launcher.enums import GameChannel
|
||||
from vollerei.hsr.launcher import api
|
||||
from vollerei import paths
|
||||
from vollerei.utils import download
|
||||
@ -189,7 +188,7 @@ class Game(GameABC):
|
||||
cfg_file = self._path.joinpath("config.ini")
|
||||
if cfg_file.exists():
|
||||
cfg = ConfigFile(cfg_file)
|
||||
cfg.set("General", "game_version", self.get_version_str())
|
||||
cfg.set("general", "game_version", self.get_version_str())
|
||||
cfg.save()
|
||||
else:
|
||||
cfg = ConfigParser()
|
||||
@ -307,7 +306,7 @@ class Game(GameABC):
|
||||
pass
|
||||
return voicepacks
|
||||
|
||||
def get_remote_game(self, pre_download: bool = False) -> resource.Game:
|
||||
def get_remote_game(self, pre_download: bool = False) -> resource.Main | resource.PreDownload:
|
||||
"""
|
||||
Gets the current game information from remote.
|
||||
|
||||
@ -316,17 +315,17 @@ class Game(GameABC):
|
||||
Defaults to False.
|
||||
|
||||
Returns:
|
||||
A `Game` object that contains the game information.
|
||||
A `Main` or `PreDownload` object that contains the game information.
|
||||
"""
|
||||
channel = self._channel_override or self.get_channel()
|
||||
if pre_download:
|
||||
game = api.get_resource(channel=channel).pre_download_game
|
||||
game = api.get_game_package(channel=channel).pre_download
|
||||
if not game:
|
||||
raise PreDownloadNotAvailable("Pre-download version is not available.")
|
||||
return game
|
||||
return api.get_resource(channel=channel).game
|
||||
return api.get_game_package(channel=channel).main
|
||||
|
||||
def get_update(self, pre_download: bool = False) -> resource.Diff | None:
|
||||
def get_update(self, pre_download: bool = False) -> resource.Patch | None:
|
||||
"""
|
||||
Gets the current game update.
|
||||
|
||||
@ -335,7 +334,7 @@ class Game(GameABC):
|
||||
Defaults to False.
|
||||
|
||||
Returns:
|
||||
A `Diff` object that contains the update information or
|
||||
A `Patch` object that contains the update information or
|
||||
`None` if the game is not installed or already up-to-date.
|
||||
"""
|
||||
if not self.is_installed():
|
||||
@ -345,9 +344,9 @@ class Game(GameABC):
|
||||
if self._version_override
|
||||
else self.get_version_str()
|
||||
)
|
||||
for diff in self.get_remote_game(pre_download=pre_download).diffs:
|
||||
if diff.version == version:
|
||||
return diff
|
||||
for patch in self.get_remote_game(pre_download=pre_download).patches:
|
||||
if patch.version == version:
|
||||
return patch
|
||||
return None
|
||||
|
||||
def _repair_file(self, file: PathLike, game: resource.Game) -> None:
|
||||
@ -486,10 +485,10 @@ class Game(GameABC):
|
||||
functions.apply_update_archive(self, archive_file, auto_repair=auto_repair)
|
||||
|
||||
def install_update(
|
||||
self, update_info: resource.Diff = None, auto_repair: bool = True
|
||||
self, update_info: resource.Patch = None, auto_repair: bool = True
|
||||
):
|
||||
"""
|
||||
Installs an update from a `Diff` object.
|
||||
Installs an update from a `Patch` object.
|
||||
|
||||
You may want to download the update manually and pass it to
|
||||
`apply_update_archive()` instead for better control, and after that
|
||||
@ -506,19 +505,20 @@ class Game(GameABC):
|
||||
update_info = self.get_update()
|
||||
if not update_info or update_info.version == self.get_version_str():
|
||||
raise GameAlreadyUpdatedError("Game is already updated.")
|
||||
update_url = update_info.game_pkgs[0].url
|
||||
# Base game update
|
||||
archive_file = self.cache.joinpath(update_info.name)
|
||||
download(update_info.path, archive_file)
|
||||
archive_file = self.cache.joinpath(PurePath(update_url).name)
|
||||
download(update_url, archive_file)
|
||||
self.apply_update_archive(archive_file=archive_file, auto_repair=auto_repair)
|
||||
# Get installed voicepacks
|
||||
installed_voicepacks = self.get_installed_voicepacks()
|
||||
# Voicepack update
|
||||
for remote_voicepack in update_info.voice_packs:
|
||||
for remote_voicepack in update_info.audio_pkgs:
|
||||
if remote_voicepack.language not in installed_voicepacks:
|
||||
continue
|
||||
# Voicepack is installed, update it
|
||||
archive_file = self.cache.joinpath(remote_voicepack.name)
|
||||
download(remote_voicepack.path, archive_file)
|
||||
archive_file = self.cache.joinpath(PurePath(remote_voicepack.url).name)
|
||||
download(remote_voicepack.url, archive_file)
|
||||
self.apply_update_archive(
|
||||
archive_file=archive_file, auto_repair=auto_repair
|
||||
)
|
||||
|
@ -34,6 +34,8 @@ class HDiffPatch:
|
||||
return "windows32"
|
||||
case "x86_64":
|
||||
return "windows64"
|
||||
case "AMD64":
|
||||
return "windows64"
|
||||
case "arm":
|
||||
return "windows_arm32"
|
||||
case "arm64":
|
||||
|
Loading…
Reference in New Issue
Block a user