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:
tretrauit 2024-09-10 16:47:20 +07:00
parent 08c51d2fd8
commit 8ff2a388d7
12 changed files with 213 additions and 500 deletions

View File

@ -1,8 +1,10 @@
import traceback
from cleo.commands.command import Command from cleo.commands.command import Command
from cleo.helpers import option, argument from cleo.helpers import option, argument
from copy import deepcopy from copy import deepcopy
from pathlib import PurePath
from platform import system from platform import system
from vollerei.hsr.launcher.enums import GameChannel from vollerei.common.enums import GameChannel
from vollerei.cli import utils from vollerei.cli import utils
from vollerei.exceptions.game import GameError from vollerei.exceptions.game import GameError
from vollerei.hsr import Game, Patcher from vollerei.hsr import Game, Patcher
@ -434,11 +436,12 @@ class UpdateCommand(Command):
update_diff = State.game.get_update(pre_download=pre_download) update_diff = State.game.get_update(pre_download=pre_download)
game_info = State.game.get_remote_game(pre_download=pre_download) game_info = State.game.get_remote_game(pre_download=pre_download)
except Exception as e: except Exception as e:
print(traceback.format_exc())
progress.finish( progress.finish(
f"<error>Update checking failed with following error: {e} ({e.__context__})</error>" f"<error>Update checking failed with following error: {e} ({e.__context__})</error>"
) )
return 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>") progress.finish("<comment>Game is already updated.</comment>")
return return
progress.finish("<comment>Update available.</comment>") 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>" f"The current version is: <comment>{State.game.get_version_str()}</comment>"
) )
self.line( 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?"): if not self.confirm("Do you want to update the game?"):
self.line("<error>Update aborted.</error>") self.line("<error>Update aborted.</error>")
return return
self.line("Downloading update package...") 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: try:
download_result = utils.download( 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: except Exception as e:
self.line_error(f"<error>Couldn't download update: {e}</error>") self.line_error(f"<error>Couldn't download update: {e}</error>")
@ -478,14 +482,14 @@ class UpdateCommand(Command):
# Get installed voicepacks # Get installed voicepacks
installed_voicepacks = State.game.get_installed_voicepacks() installed_voicepacks = State.game.get_installed_voicepacks()
# Voicepack update # 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: if remote_voicepack.language not in installed_voicepacks:
continue continue
# Voicepack is installed, update it # 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: try:
download_result = utils.download( 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: except Exception as e:
self.line_error(f"<error>Couldn't download update: {e}</error>") self.line_error(f"<error>Couldn't download update: {e}</error>")

View File

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

View File

@ -1,409 +1,134 @@
"""
Class wrapper for API endpoint /resource
"""
from vollerei.common.enums import VoicePackLanguage 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: class Game:
latest: Latest def __init__(self, id: str, biz: str):
diffs: list[Diff] self.id = id
self.biz = biz
def __init__(self, latest: Latest, diffs: list[Diff]) -> None:
self.latest = latest
self.diffs = diffs
@staticmethod @staticmethod
def from_dict(data: dict) -> "Game": def from_dict(data: dict) -> "Game":
return Game( return Game(id=data["id"], biz=data["biz"])
Latest.from_dict(data["latest"]), [Diff.from_dict(i) for i in data["diffs"]]
)
class Plugin: class GamePackage:
name: str def __init__(self, url: str, md5: str, size: int, decompressed_size: int):
# str but checked for empty string self.url = url
version: str | None self.md5 = md5
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
self.size = size self.size = size
self.md5 = md5 self.decompressed_size = decompressed_size
self.entry = entry
self.package_size = package_size
@staticmethod @staticmethod
def from_dict(data: dict) -> "Plugin": def from_dict(data: dict) -> "GamePackage":
return Plugin( return GamePackage(
data["name"], url=data["url"],
data["version"] if data["version"] != "" else None, md5=data["md5"],
data["path"], size=int(data["size"]),
int(data["size"]), decompressed_size=int(data["decompressed_size"]),
data["md5"],
data["entry"] if data["entry"] != "" else None,
int(data["package_size"]),
) )
class LauncherPlugin: class AudioPackage:
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]
def __init__( def __init__(
self, self,
game: Game, language: VoicePackLanguage,
plugin: Plugin, url: str,
web_url: str, md5: str,
force_update: None, size: int,
pre_download_game: Game | None, decompressed_size: int,
deprecated_packages: list[DeprecatedPackage], ):
sdk: None, self.language = language
deprecated_files: list[DeprecatedFile], self.url = url
) -> None: self.md5 = md5
self.game = game self.size = size
self.plugin = plugin self.decompressed_size = decompressed_size
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
@staticmethod @staticmethod
def from_dict(json: dict) -> "Resource": def from_dict(data: dict) -> "AudioPackage":
return Resource( return AudioPackage(
Game.from_dict(json["game"]), language=VoicePackLanguage.from_remote_str(data["language"]),
LauncherPlugin.from_dict(json["plugin"]), url=data["url"],
json["web_url"], md5=data["md5"],
json["force_update"], size=int(data["size"]),
( decompressed_size=int(data["decompressed_size"]),
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"]],
) )
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

View File

@ -1,6 +1,11 @@
from enum import Enum from enum import Enum
class GameChannel(Enum):
Overseas = 0
China = 1
class VoicePackLanguage(Enum): class VoicePackLanguage(Enum):
Japanese = "ja-jp" Japanese = "ja-jp"
Chinese = "zh-cn" Chinese = "zh-cn"

View File

@ -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 = [ TELEMETRY_HOSTS = [
# Global # Global
"log-upload-os.hoyoverse.com", "log-upload-os.hoyoverse.com",

View File

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

View File

@ -1,9 +0,0 @@
from enum import Enum
class GameChannel(Enum):
Global = 0
Asia = 1
Taiwan = 2
Korea = 3
China = 4

View File

@ -1,26 +1,4 @@
class LAUNCHER_API: LATEST_VERSION = (2, 5, 0)
"""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)
MD5SUMS = { MD5SUMS = {
"1.0.5": { "1.0.5": {
"cn": { "cn": {

View File

@ -1,13 +1,10 @@
import requests from vollerei.common.api import get_game_packages, resource
from vollerei.common.enums import GameChannel
from vollerei.common.api import Resource
from vollerei.hsr.constants import LAUNCHER_API
from vollerei.hsr.launcher.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. Default channel is overseas.
@ -17,15 +14,7 @@ def get_resource(channel: GameChannel = GameChannel.Overseas) -> Resource:
Returns: Returns:
Resource: Game resource information. Resource: Game resource information.
""" """
resource_path: dict = None game_packages = get_game_packages(channel=channel)
match channel: for package in game_packages:
case GameChannel.Overseas: if "hkrpg" in package.game.biz:
resource_path = LAUNCHER_API.OS return package
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"]
)

View File

@ -1,6 +0,0 @@
from enum import Enum
class GameChannel(Enum):
Overseas = 0
China = 1

View File

@ -3,12 +3,12 @@ from configparser import ConfigParser
from hashlib import md5 from hashlib import md5
from io import IOBase from io import IOBase
from os import PathLike from os import PathLike
from pathlib import Path from pathlib import Path, PurePath
from shutil import move, copyfile from shutil import move, copyfile
from vollerei.abc.launcher.game import GameABC from vollerei.abc.launcher.game import GameABC
from vollerei.common import ConfigFile, functions from vollerei.common import ConfigFile, functions
from vollerei.common.api import resource from vollerei.common.api import resource
from vollerei.common.enums import VoicePackLanguage from vollerei.common.enums import VoicePackLanguage, GameChannel
from vollerei.exceptions.game import ( from vollerei.exceptions.game import (
GameAlreadyUpdatedError, GameAlreadyUpdatedError,
GameNotInstalledError, GameNotInstalledError,
@ -16,7 +16,6 @@ from vollerei.exceptions.game import (
ScatteredFilesNotAvailableError, ScatteredFilesNotAvailableError,
) )
from vollerei.hsr.constants import MD5SUMS from vollerei.hsr.constants import MD5SUMS
from vollerei.hsr.launcher.enums import GameChannel
from vollerei.hsr.launcher import api from vollerei.hsr.launcher import api
from vollerei import paths from vollerei import paths
from vollerei.utils import download from vollerei.utils import download
@ -189,7 +188,7 @@ class Game(GameABC):
cfg_file = self._path.joinpath("config.ini") cfg_file = self._path.joinpath("config.ini")
if cfg_file.exists(): if cfg_file.exists():
cfg = ConfigFile(cfg_file) cfg = ConfigFile(cfg_file)
cfg.set("General", "game_version", self.get_version_str()) cfg.set("general", "game_version", self.get_version_str())
cfg.save() cfg.save()
else: else:
cfg = ConfigParser() cfg = ConfigParser()
@ -307,7 +306,7 @@ class Game(GameABC):
pass pass
return voicepacks 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. Gets the current game information from remote.
@ -316,17 +315,17 @@ class Game(GameABC):
Defaults to False. Defaults to False.
Returns: 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() channel = self._channel_override or self.get_channel()
if pre_download: if pre_download:
game = api.get_resource(channel=channel).pre_download_game game = api.get_game_package(channel=channel).pre_download
if not game: if not game:
raise PreDownloadNotAvailable("Pre-download version is not available.") raise PreDownloadNotAvailable("Pre-download version is not available.")
return game 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. Gets the current game update.
@ -335,7 +334,7 @@ class Game(GameABC):
Defaults to False. Defaults to False.
Returns: 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. `None` if the game is not installed or already up-to-date.
""" """
if not self.is_installed(): if not self.is_installed():
@ -345,9 +344,9 @@ class Game(GameABC):
if self._version_override if self._version_override
else self.get_version_str() else self.get_version_str()
) )
for diff in self.get_remote_game(pre_download=pre_download).diffs: for patch in self.get_remote_game(pre_download=pre_download).patches:
if diff.version == version: if patch.version == version:
return diff return patch
return None return None
def _repair_file(self, file: PathLike, game: resource.Game) -> 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) functions.apply_update_archive(self, archive_file, auto_repair=auto_repair)
def install_update( 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 You may want to download the update manually and pass it to
`apply_update_archive()` instead for better control, and after that `apply_update_archive()` instead for better control, and after that
@ -506,19 +505,20 @@ class Game(GameABC):
update_info = self.get_update() update_info = self.get_update()
if not update_info or update_info.version == self.get_version_str(): if not update_info or update_info.version == self.get_version_str():
raise GameAlreadyUpdatedError("Game is already updated.") raise GameAlreadyUpdatedError("Game is already updated.")
update_url = update_info.game_pkgs[0].url
# Base game update # Base game update
archive_file = self.cache.joinpath(update_info.name) archive_file = self.cache.joinpath(PurePath(update_url).name)
download(update_info.path, archive_file) download(update_url, archive_file)
self.apply_update_archive(archive_file=archive_file, auto_repair=auto_repair) self.apply_update_archive(archive_file=archive_file, auto_repair=auto_repair)
# Get installed voicepacks # Get installed voicepacks
installed_voicepacks = self.get_installed_voicepacks() installed_voicepacks = self.get_installed_voicepacks()
# Voicepack update # 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: if remote_voicepack.language not in installed_voicepacks:
continue continue
# Voicepack is installed, update it # Voicepack is installed, update it
archive_file = self.cache.joinpath(remote_voicepack.name) archive_file = self.cache.joinpath(PurePath(remote_voicepack.url).name)
download(remote_voicepack.path, archive_file) download(remote_voicepack.url, archive_file)
self.apply_update_archive( self.apply_update_archive(
archive_file=archive_file, auto_repair=auto_repair archive_file=archive_file, auto_repair=auto_repair
) )

View File

@ -34,6 +34,8 @@ class HDiffPatch:
return "windows32" return "windows32"
case "x86_64": case "x86_64":
return "windows64" return "windows64"
case "AMD64":
return "windows64"
case "arm": case "arm":
return "windows_arm32" return "windows_arm32"
case "arm64": case "arm64":