refactor: more cleanup
feat: initial support for callbacks
This commit is contained in:
parent
f909297ae0
commit
2abbdf066b
|
@ -2,6 +2,26 @@ from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class GameVariant(Enum):
|
class GameVariant(Enum):
|
||||||
INTERNATIONAL = 1
|
INTERNATIONAL = "international"
|
||||||
CHINESE = 2
|
CHINESE = "chinese"
|
||||||
BILIBILI = 3
|
BILIBILI = "bilibili"
|
||||||
|
|
||||||
|
|
||||||
|
class GameVersionLocation(Enum):
|
||||||
|
GAME_FILE = "globalgamemanagers"
|
||||||
|
LAUNCHER_CONFIG = "config.ini"
|
||||||
|
|
||||||
|
|
||||||
|
class VoicepackArchiveType(Enum):
|
||||||
|
FULL = 0
|
||||||
|
UPDATE = 1
|
||||||
|
|
||||||
|
|
||||||
|
class GameUpdateStatus(Enum):
|
||||||
|
DOWNLOADING_PATCHER = 0
|
||||||
|
PREPARING_PATCH = 1
|
||||||
|
PATCHING = 2
|
||||||
|
PREPARING_REMOVE_UNUSED = 3
|
||||||
|
REMOVING_UNUSED = 4
|
||||||
|
EXTRACTING = 5
|
||||||
|
COMPLETED = 6
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import platform
|
|
||||||
import aiohttp
|
|
||||||
import zipfile
|
import zipfile
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from configparser import ConfigParser
|
|
||||||
import worthless.helper as helper
|
import worthless.helper as helper
|
||||||
from worthless import constants
|
from worthless import constants
|
||||||
from worthless.launcher import Launcher
|
from worthless.launcher import Launcher
|
||||||
from worthless.enums import GameVariant
|
from worthless.classes import installer
|
||||||
|
from worthless.enums import GameVariant, VoicepackArchiveType, GameUpdateStatus
|
||||||
from worthless.hdiffpatch import HDiffPatch
|
from worthless.hdiffpatch import HDiffPatch
|
||||||
from worthless.launcherconfig import LauncherConfig
|
from worthless.launcherconfig import LauncherConfig
|
||||||
|
|
||||||
|
@ -31,23 +31,24 @@ class GameManager:
|
||||||
if not isinstance(game_dir, Path):
|
if not isinstance(game_dir, Path):
|
||||||
game_dir = Path(game_dir)
|
game_dir = Path(game_dir)
|
||||||
self._game_dir = game_dir
|
self._game_dir = game_dir
|
||||||
if not data_dir:
|
if data_dir:
|
||||||
|
if not isinstance(data_dir, Path):
|
||||||
|
data_dir = Path(data_dir)
|
||||||
|
self._cache_path = data_dir.joinpath("Temp/Installer/")
|
||||||
|
else:
|
||||||
self._cache_path = Path(constants.APPDIRS.user_cache_dir).joinpath(
|
self._cache_path = Path(constants.APPDIRS.user_cache_dir).joinpath(
|
||||||
"Installer"
|
"Installer"
|
||||||
)
|
)
|
||||||
else:
|
Path(self._cache_path).mkdir(parents=True, exist_ok=True)
|
||||||
if not isinstance(data_dir, Path):
|
|
||||||
data_dir = Path(data_dir)
|
|
||||||
self.temp_path = data_dir.joinpath("Temp/Installer/")
|
|
||||||
Path(self.temp_path).mkdir(parents=True, exist_ok=True)
|
|
||||||
config_file = self._game_dir.joinpath("config.ini")
|
|
||||||
self._config_file = config_file
|
|
||||||
self._download_chunk = 8192
|
self._download_chunk = 8192
|
||||||
self._variant = variant
|
self._variant = variant
|
||||||
self._version = None
|
self._version = None
|
||||||
self._launcher = Launcher(self._game_dir, variant=variant)
|
self._launcher = Launcher(variant=variant)
|
||||||
self._hdiffpatch = HDiffPatch(data_dir=data_dir)
|
self._hdiffpatch = HDiffPatch(data_dir=data_dir)
|
||||||
self._config = LauncherConfig(self._config_file, self._version)
|
self._game_config_file = self._game_dir.joinpath("config.ini")
|
||||||
|
self._game_config = LauncherConfig(
|
||||||
|
self._game_config_file, game_version=self.version
|
||||||
|
)
|
||||||
self._game_version_re = re.compile(r"([1-9]+\.[0-9]+\.[0-9]+)_\d+_\d+")
|
self._game_version_re = re.compile(r"([1-9]+\.[0-9]+\.[0-9]+)_\d+_\d+")
|
||||||
|
|
||||||
async def _download_file(
|
async def _download_file(
|
||||||
|
@ -62,25 +63,30 @@ class GameManager:
|
||||||
await helper.download_file(
|
await helper.download_file(
|
||||||
file_url,
|
file_url,
|
||||||
file_name,
|
file_name,
|
||||||
self.temp_path,
|
self._cache_path,
|
||||||
file_len=file_len,
|
file_len=file_len,
|
||||||
overwrite=overwrite,
|
overwrite=overwrite,
|
||||||
chunks=self._download_chunk,
|
chunks=self._download_chunk,
|
||||||
)
|
)
|
||||||
|
|
||||||
def read_version_from_config(self):
|
def _read_version_from_config(self) -> str | None:
|
||||||
if not self._config_file.exists():
|
"""
|
||||||
raise FileNotFoundError(f"Config file {self._config_file} not found")
|
Reads the version from config.ini
|
||||||
cfg = ConfigParser()
|
|
||||||
cfg.read(str(self._config_file))
|
|
||||||
return cfg.get("General", "game_version")
|
|
||||||
|
|
||||||
def read_version_from_game_file(self, globalgamemanagers: Path | bytes) -> str:
|
:return:
|
||||||
|
"""
|
||||||
|
if not self._game_config_file.exists():
|
||||||
|
raise FileNotFoundError(f"Config file {self._game_config_file} not found")
|
||||||
|
return self._game_config.get("General", "game_version", fallback=None)
|
||||||
|
|
||||||
|
def _read_version_from_game_file(
|
||||||
|
self, globalgamemanagers: Path | bytes
|
||||||
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
|
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
|
||||||
|
|
||||||
Uses `An Anime Game Launcher` method to read the version:
|
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
|
https://github.com/an-anime-team/an-anime-game-launcher/blob/main/src/ts/Game.ts#L26
|
||||||
|
|
||||||
:return: Game version (e.g. 1.0.0)
|
:return: Game version (e.g. 1.0.0)
|
||||||
"""
|
"""
|
||||||
|
@ -90,334 +96,23 @@ class GameManager:
|
||||||
data = globalgamemanagers.decode(encoding="ascii", errors="ignore")
|
data = globalgamemanagers.decode(encoding="ascii", errors="ignore")
|
||||||
result = self._game_version_re.search(data)
|
result = self._game_version_re.search(data)
|
||||||
if not result:
|
if not result:
|
||||||
raise ValueError("Could not find version in game file")
|
return None
|
||||||
return result.group(1)
|
return result.group(1)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def voiceover_lang_translate(lang: str, base_language="game") -> str:
|
|
||||||
"""
|
|
||||||
Translates the voiceover language to the language code used by the game.
|
|
||||||
:param lang: Language to translate
|
|
||||||
:param base_language: Base language type (game/locale/both)
|
|
||||||
:return: Language code
|
|
||||||
"""
|
|
||||||
if base_language == "game" or base_language == "both":
|
|
||||||
match lang.lower():
|
|
||||||
case "english(us)":
|
|
||||||
return "en-us"
|
|
||||||
case "japanese":
|
|
||||||
return "ja-jp"
|
|
||||||
case "chinese":
|
|
||||||
return "zh-cn"
|
|
||||||
case "korean":
|
|
||||||
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
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_voiceover_archive_language(voiceover_archive: str | Path) -> str:
|
|
||||||
if isinstance(voiceover_archive, str | Path):
|
|
||||||
voiceover_archive = Path(voiceover_archive).resolve()
|
|
||||||
if not voiceover_archive.exists():
|
|
||||||
raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found")
|
|
||||||
with zipfile.ZipFile(voiceover_archive, "r") as f:
|
|
||||||
for file in zipfile.Path(f).iterdir():
|
|
||||||
if file.name.endswith("_pkg_version"):
|
|
||||||
return file.name.split("_")[1]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_voiceover_archive_type(voiceover_archive: str | Path) -> bool:
|
|
||||||
"""
|
|
||||||
Gets voiceover archive type.
|
|
||||||
:param voiceover_archive:
|
|
||||||
:return: True if this is a full archive, else False.
|
|
||||||
"""
|
|
||||||
vo_lang = GameManager.get_voiceover_archive_language(voiceover_archive)
|
|
||||||
with zipfile.ZipFile(voiceover_archive, "r") as f:
|
|
||||||
archive_path = zipfile.Path(f)
|
|
||||||
files = f.read("Audio_{}_pkg_version".format(vo_lang)).decode().split("\n")
|
|
||||||
for file in files:
|
|
||||||
if (
|
|
||||||
file.strip()
|
|
||||||
and not archive_path.joinpath(
|
|
||||||
json.loads(file)["remoteName"]
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def set_download_chunk(self, chunk: int):
|
|
||||||
self._download_chunk = chunk
|
|
||||||
|
|
||||||
def get_game_data_name(self):
|
|
||||||
match self._variant:
|
|
||||||
case GameVariant.INTERNATIONAL:
|
|
||||||
return "GenshinImpact_Data/"
|
|
||||||
case GameVariant.CHINESE:
|
|
||||||
return "YuanShen_Data/"
|
|
||||||
case GameVariant.BILIBILI:
|
|
||||||
return "YuanShen_Data/"
|
|
||||||
|
|
||||||
def get_game_data_path(self) -> Path:
|
|
||||||
return self._game_dir.joinpath(self.get_game_data_name())
|
|
||||||
|
|
||||||
def get_game_archive_version(self, game_archive: str | Path):
|
|
||||||
game_archive = Path(game_archive)
|
|
||||||
if not game_archive.is_file():
|
|
||||||
raise FileNotFoundError(f"Game archive {game_archive} not found")
|
|
||||||
with zipfile.ZipFile(game_archive, "r") as f:
|
|
||||||
return self.read_version_from_game_file(
|
|
||||||
f.read(self.get_game_data_name() + "globalgamemanagers")
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_game_version(self) -> str | None:
|
|
||||||
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
|
|
||||||
if not globalgamemanagers.exists():
|
|
||||||
try:
|
|
||||||
return self.read_version_from_config()
|
|
||||||
except FileNotFoundError:
|
|
||||||
return
|
|
||||||
return self.read_version_from_game_file(globalgamemanagers)
|
|
||||||
|
|
||||||
def get_installed_voiceovers(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
async def update_game(self, game_archive: str | Path):
|
|
||||||
if not self.get_game_data_path().exists():
|
|
||||||
raise FileNotFoundError(f"Game not found in {self._game_dir}")
|
|
||||||
if isinstance(game_archive, str | Path):
|
|
||||||
game_archive = Path(game_archive).resolve()
|
|
||||||
if not game_archive.exists():
|
|
||||||
raise FileNotFoundError(f"Update archive {game_archive} not found")
|
|
||||||
|
|
||||||
archive = zipfile.ZipFile(game_archive, "r")
|
|
||||||
|
|
||||||
if not self._hdiffpatch.get_hpatchz_executable():
|
|
||||||
await self._hdiffpatch.download_latest_release()
|
|
||||||
|
|
||||||
files = archive.namelist()
|
|
||||||
# Don't extract these files (they're useless and if the game isn't patched then it'll
|
|
||||||
# raise 31-4xxx error ingame)
|
|
||||||
for file in ["deletefiles.txt", "hdifffiles.txt"]:
|
|
||||||
try:
|
|
||||||
files.remove(file)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# hdiffpatch implementation
|
|
||||||
hdifffiles = []
|
|
||||||
for x in (
|
|
||||||
(await asyncio.to_thread(archive.read, "hdifffiles.txt"))
|
|
||||||
.decode()
|
|
||||||
.split("\n")
|
|
||||||
):
|
|
||||||
if x:
|
|
||||||
hdifffiles.append(json.loads(x)["remoteName"])
|
|
||||||
patch_jobs = []
|
|
||||||
for file in hdifffiles:
|
|
||||||
current_game_file = self._game_dir.joinpath(file)
|
|
||||||
if not current_game_file.exists():
|
|
||||||
# Not patching since we don't have the file
|
|
||||||
continue
|
|
||||||
|
|
||||||
patch_file = str(file) + ".hdiff"
|
|
||||||
|
|
||||||
async def extract_and_patch(old_file, diff_file):
|
|
||||||
diff_path = self.temp_path.joinpath(diff_file)
|
|
||||||
if diff_path.is_file():
|
|
||||||
diff_path.unlink(missing_ok=True)
|
|
||||||
await asyncio.to_thread(archive.extract, diff_file, self.temp_path)
|
|
||||||
patch_path = self.temp_path.joinpath(diff_file)
|
|
||||||
old_suffix = old_file.suffix
|
|
||||||
old_file = await old_file.rename(old_file.with_suffix(".bak"))
|
|
||||||
proc = await self._hdiffpatch.patch_file(
|
|
||||||
old_file, old_file.with_suffix(old_suffix), patch_path, wait=True
|
|
||||||
)
|
|
||||||
patch_path.unlink()
|
|
||||||
if proc.returncode != 0:
|
|
||||||
# Let the game download the file.
|
|
||||||
await old_file.rename(old_file.with_suffix(old_suffix))
|
|
||||||
return
|
|
||||||
await old_file.unlink()
|
|
||||||
|
|
||||||
files.remove(patch_file)
|
|
||||||
patch_jobs.append(extract_and_patch(current_game_file, patch_file))
|
|
||||||
|
|
||||||
await asyncio.gather(*patch_jobs)
|
|
||||||
|
|
||||||
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
|
|
||||||
for file in deletefiles:
|
|
||||||
current_game_file = Path(self._game_dir.joinpath(file))
|
|
||||||
if not current_game_file.exists():
|
|
||||||
continue
|
|
||||||
if current_game_file.is_file():
|
|
||||||
current_game_file.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
await asyncio.to_thread(archive.extractall, self._game_dir, members=files)
|
|
||||||
archive.close()
|
|
||||||
# Update game version on local variable.
|
|
||||||
self._version = self.get_game_version()
|
|
||||||
self.set_version_config()
|
|
||||||
|
|
||||||
def set_version_config(self, version: str = None):
|
|
||||||
if not version:
|
|
||||||
version = self._version
|
|
||||||
self._config.set_game_version(version)
|
|
||||||
self._config.save()
|
|
||||||
|
|
||||||
async def download_full_game(self, pre_download=False):
|
|
||||||
game = await self._get_game(pre_download)
|
|
||||||
archive_name = game.latest.path.split("/")[-1]
|
|
||||||
await self._download_file(game.latest.path, archive_name, game.latest.size)
|
|
||||||
|
|
||||||
async def download_full_voiceover(self, language: str, pre_download=False):
|
|
||||||
game = await self._get_game(pre_download)
|
|
||||||
translated_lang = self.voiceover_lang_translate(language)
|
|
||||||
for vo in game.latest.voice_packs:
|
|
||||||
if vo.language == translated_lang:
|
|
||||||
await self._download_file(vo.path, vo.get_name(), vo.size)
|
|
||||||
|
|
||||||
def uninstall_game(self):
|
|
||||||
shutil.rmtree(self._game_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
def _extract_game_file(self, archive: str | Path):
|
|
||||||
if isinstance(archive, str):
|
|
||||||
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._game_dir)
|
|
||||||
|
|
||||||
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 self.get_game_data_path().exists():
|
|
||||||
raise FileNotFoundError(f"Game not found in {self._game_dir}")
|
|
||||||
self._extract_game_file(voiceover_archive)
|
|
||||||
|
|
||||||
async 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._game_dir}")
|
|
||||||
self.uninstall_game()
|
|
||||||
|
|
||||||
self._game_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
self._extract_game_file(game_archive)
|
|
||||||
self._version = self.get_game_version()
|
|
||||||
self.set_version_config()
|
|
||||||
|
|
||||||
async def _get_game_version(self):
|
|
||||||
if self._version:
|
|
||||||
from_version = self._version
|
|
||||||
else:
|
|
||||||
from_version = self._version = self.get_game_version()
|
|
||||||
return from_version
|
|
||||||
|
|
||||||
async def _get_game_resource(self):
|
async def _get_game_resource(self):
|
||||||
game_resource = await self._launcher.get_resource_info()
|
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")
|
||||||
return game_resource
|
return game_resource
|
||||||
|
|
||||||
async def _get_game(self, pre_download=False):
|
async def _get_game(self, pre_download=False) -> installer.Game:
|
||||||
game_resource = await self._get_game_resource()
|
game_resource = await self._get_game_resource()
|
||||||
game = game_resource.game
|
game = game_resource.game
|
||||||
if pre_download:
|
if pre_download:
|
||||||
game = game_resource.pre_download_game
|
game = game_resource.pre_download_game
|
||||||
return game
|
return game
|
||||||
|
|
||||||
async def download_game_update(self, from_version: str = None, pre_download=False):
|
async def _verify_from_pkg_version(self, pkg_version: Path, ignore_mismatch=False):
|
||||||
if not from_version:
|
|
||||||
from_version = await self._get_game_version()
|
|
||||||
game = await self._get_game(pre_download=pre_download)
|
|
||||||
if self._version == game.latest.version:
|
|
||||||
raise ValueError("Game is already up to date.")
|
|
||||||
diff_archive = await self.get_game_diff_archive(from_version, pre_download)
|
|
||||||
if diff_archive is None:
|
|
||||||
raise ValueError(
|
|
||||||
"Game diff archive is not available for this version, please reinstall."
|
|
||||||
)
|
|
||||||
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, pre_download=False
|
|
||||||
):
|
|
||||||
if not from_version:
|
|
||||||
from_version = await self._get_game_version()
|
|
||||||
diff_archive = await self.get_voiceover_diff_archive(
|
|
||||||
language, from_version, pre_download
|
|
||||||
)
|
|
||||||
if diff_archive is None:
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_voiceover_diff_archive(
|
|
||||||
self, lang: str, from_version: str = None, pre_download=False
|
|
||||||
):
|
|
||||||
"""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 not from_version:
|
|
||||||
from_version = await self._get_game_version()
|
|
||||||
game = await self._get_game(pre_download=pre_download)
|
|
||||||
translated_lang = self.voiceover_lang_translate(lang)
|
|
||||||
for v in game.diffs:
|
|
||||||
if v.version != from_version:
|
|
||||||
continue
|
|
||||||
for vo in v.voice_packs:
|
|
||||||
if vo.language == translated_lang:
|
|
||||||
return vo
|
|
||||||
|
|
||||||
async def get_game_diff_archive(self, from_version: str = None, pre_download=False):
|
|
||||||
"""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 not from_version:
|
|
||||||
from_version = await self._get_game_version()
|
|
||||||
game = await self._get_game(pre_download=pre_download)
|
|
||||||
for v in game.diffs:
|
|
||||||
if v.version == from_version:
|
|
||||||
return v
|
|
||||||
|
|
||||||
async def verify_from_pkg_version(self, pkg_version: Path, ignore_mismatch=False):
|
|
||||||
contents = pkg_version.read_text()
|
contents = pkg_version.read_text()
|
||||||
|
|
||||||
def calculate_md5(file_to_calculate):
|
def calculate_md5(file_to_calculate):
|
||||||
|
@ -461,10 +156,333 @@ class GameManager:
|
||||||
|
|
||||||
return None if not failed_files else failed_files
|
return None if not failed_files else failed_files
|
||||||
|
|
||||||
async def verify_game(self, pkg_version: str | Path = None, ignore_mismatch=False):
|
@property
|
||||||
|
def version(self) -> str | None:
|
||||||
|
if not self._version:
|
||||||
|
self._version = self.get_current_game_version()
|
||||||
|
return self._version
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def voicepack_lang_translate(lang: str, base_language="game") -> str | None:
|
||||||
|
"""
|
||||||
|
Translates the voicepack language to the language code used by the game.
|
||||||
|
|
||||||
|
:param lang: Language to translate
|
||||||
|
:param base_language: Base language type (game/locale/both)
|
||||||
|
:return: Language code
|
||||||
|
"""
|
||||||
|
if base_language == "game" or base_language == "both":
|
||||||
|
match lang.lower():
|
||||||
|
case "english(us)":
|
||||||
|
return "en-us"
|
||||||
|
case "japanese":
|
||||||
|
return "ja-jp"
|
||||||
|
case "chinese":
|
||||||
|
return "zh-cn"
|
||||||
|
case "korean":
|
||||||
|
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
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_voicepack_archive_language(archive: PathLike) -> str:
|
||||||
|
if not isinstance(archive, Path):
|
||||||
|
archive = Path(archive).resolve()
|
||||||
|
if not archive.exists():
|
||||||
|
raise FileNotFoundError(f"Voiceover archive {archive} not found")
|
||||||
|
with zipfile.ZipFile(archive, "r") as f:
|
||||||
|
for file in zipfile.Path(f).iterdir():
|
||||||
|
if file.name.startswith("Audio_") and file.name.endswith(
|
||||||
|
"_pkg_version"
|
||||||
|
):
|
||||||
|
# Audio_<language>_pkg_version
|
||||||
|
return file.name.split("_")[1]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_voicepack_archive_type(archive: str | Path) -> VoicepackArchiveType:
|
||||||
|
"""
|
||||||
|
Gets voicepack archive type.
|
||||||
|
:param archive: The voicepack archive to get type
|
||||||
|
:return: True if this is a full archive, else False.
|
||||||
|
"""
|
||||||
|
vo_lang = GameManager.get_voicepack_archive_language(archive)
|
||||||
|
with zipfile.ZipFile(archive, "r") as f:
|
||||||
|
archive_path = zipfile.Path(f)
|
||||||
|
files = f.read("Audio_{}_pkg_version".format(vo_lang)).decode().split("\n")
|
||||||
|
for file in files:
|
||||||
|
if (
|
||||||
|
file.strip()
|
||||||
|
and not archive_path.joinpath(
|
||||||
|
json.loads(file)["remoteName"]
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
|
return VoicepackArchiveType.UPDATE
|
||||||
|
return VoicepackArchiveType.FULL
|
||||||
|
|
||||||
|
def set_download_chunk(self, chunk: int):
|
||||||
|
self._download_chunk = chunk
|
||||||
|
|
||||||
|
def get_game_data_name(self):
|
||||||
|
match self._variant:
|
||||||
|
case GameVariant.INTERNATIONAL:
|
||||||
|
return "GenshinImpact_Data/"
|
||||||
|
case GameVariant.CHINESE:
|
||||||
|
return "YuanShen_Data/"
|
||||||
|
case GameVariant.BILIBILI:
|
||||||
|
return "YuanShen_Data/"
|
||||||
|
|
||||||
|
def get_game_data_path(self) -> Path:
|
||||||
|
return self._game_dir.joinpath(self.get_game_data_name())
|
||||||
|
|
||||||
|
def get_archive_game_version(self, game_archive: str | Path):
|
||||||
|
game_archive = Path(game_archive)
|
||||||
|
if not game_archive.is_file():
|
||||||
|
raise FileNotFoundError(f"Game archive {game_archive} not found")
|
||||||
|
with zipfile.ZipFile(game_archive, "r") as f:
|
||||||
|
return self._read_version_from_game_file(
|
||||||
|
f.read(self.get_game_data_name() + "globalgamemanagers")
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_current_game_version(self) -> str | None:
|
||||||
|
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
|
||||||
|
if not globalgamemanagers.exists():
|
||||||
|
try:
|
||||||
|
return self._read_version_from_config()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return
|
||||||
|
return self._read_version_from_game_file(globalgamemanagers)
|
||||||
|
|
||||||
|
def get_installed_voicepacks(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
Returns a list of installed voicepacks.
|
||||||
|
|
||||||
|
:return: List of installed voicepacks
|
||||||
|
"""
|
||||||
|
voicepacks = []
|
||||||
|
for file in (
|
||||||
|
self.get_game_data_path()
|
||||||
|
.joinpath("StreamingAssets/Audio/GeneratedSoundBanks/Windows")
|
||||||
|
.iterdir()
|
||||||
|
):
|
||||||
|
if file.is_dir():
|
||||||
|
voicepacks.append(file.name)
|
||||||
|
return voicepacks
|
||||||
|
|
||||||
|
async def update_game(self, game_archive: str | Path, callback=None):
|
||||||
|
if not self.get_game_data_path().exists():
|
||||||
|
raise FileNotFoundError(f"Game not found in {self._game_dir}")
|
||||||
|
if isinstance(game_archive, str | Path):
|
||||||
|
game_archive = Path(game_archive).resolve()
|
||||||
|
if not game_archive.exists():
|
||||||
|
raise FileNotFoundError(f"Update archive {game_archive} not found")
|
||||||
|
|
||||||
|
def call(status, file=None):
|
||||||
|
if not callback:
|
||||||
|
return
|
||||||
|
callback(status=status, file=file)
|
||||||
|
|
||||||
|
if not self._hdiffpatch.get_hpatchz_executable():
|
||||||
|
call(GameUpdateStatus.DOWNLOADING_PATCHER)
|
||||||
|
await self._hdiffpatch.download_latest_release()
|
||||||
|
|
||||||
|
archive = zipfile.ZipFile(game_archive, "r")
|
||||||
|
files = archive.namelist()
|
||||||
|
# Don't extract these files (they're useless and if the game isn't patched then it'll
|
||||||
|
# raise 31-4xxx error ingame)
|
||||||
|
call(GameUpdateStatus.PREPARING_PATCH)
|
||||||
|
for file in ["deletefiles.txt", "hdifffiles.txt"]:
|
||||||
|
try:
|
||||||
|
files.remove(file)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# hdiffpatch implementation
|
||||||
|
hdifffiles = []
|
||||||
|
for x in (
|
||||||
|
(await asyncio.to_thread(archive.read, "hdifffiles.txt"))
|
||||||
|
.decode()
|
||||||
|
.split("\n")
|
||||||
|
):
|
||||||
|
if x:
|
||||||
|
hdifffiles.append(json.loads(x)["remoteName"])
|
||||||
|
patch_jobs = []
|
||||||
|
for file in hdifffiles:
|
||||||
|
call(GameUpdateStatus.PREPARING_PATCH, file=file)
|
||||||
|
current_game_file = self._game_dir.joinpath(file)
|
||||||
|
if not current_game_file.exists():
|
||||||
|
# Not patching since we don't have the file
|
||||||
|
continue
|
||||||
|
|
||||||
|
patch_file = str(file) + ".hdiff"
|
||||||
|
|
||||||
|
async def extract_and_patch(old_file, diff_file):
|
||||||
|
call(GameUpdateStatus.PATCHING, file=old_file)
|
||||||
|
diff_path = self._cache_path.joinpath(diff_file)
|
||||||
|
if diff_path.is_file():
|
||||||
|
diff_path.unlink(missing_ok=True)
|
||||||
|
await asyncio.to_thread(archive.extract, diff_file, self._cache_path)
|
||||||
|
patch_path = self._cache_path.joinpath(diff_file)
|
||||||
|
old_suffix = old_file.suffix
|
||||||
|
old_file = await old_file.rename(old_file.with_suffix(".bak"))
|
||||||
|
proc = await self._hdiffpatch.patch_file(
|
||||||
|
old_file, old_file.with_suffix(old_suffix), patch_path, wait=True
|
||||||
|
)
|
||||||
|
patch_path.unlink()
|
||||||
|
if proc.returncode != 0:
|
||||||
|
# Let the game download the file.
|
||||||
|
await old_file.rename(old_file.with_suffix(old_suffix))
|
||||||
|
return
|
||||||
|
await old_file.unlink()
|
||||||
|
|
||||||
|
files.remove(patch_file)
|
||||||
|
patch_jobs.append(extract_and_patch(current_game_file, patch_file))
|
||||||
|
|
||||||
|
await asyncio.gather(*patch_jobs)
|
||||||
|
call(GameUpdateStatus.PREPARING_REMOVE_UNUSED)
|
||||||
|
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
|
||||||
|
for file in deletefiles:
|
||||||
|
current_game_file = Path(self._game_dir.joinpath(file))
|
||||||
|
if not current_game_file.exists():
|
||||||
|
continue
|
||||||
|
if current_game_file.is_file():
|
||||||
|
call(GameUpdateStatus.REMOVING_UNUSED, file=file)
|
||||||
|
current_game_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
call(GameUpdateStatus.EXTRACTING)
|
||||||
|
await asyncio.to_thread(archive.extractall, self._game_dir, members=files)
|
||||||
|
archive.close()
|
||||||
|
# Update game version on local variable.
|
||||||
|
self._version = self.get_current_game_version()
|
||||||
|
self.set_version_config()
|
||||||
|
call(GameUpdateStatus.COMPLETED)
|
||||||
|
|
||||||
|
def set_version_config(self, version: str = None):
|
||||||
|
if not version:
|
||||||
|
version = self.version
|
||||||
|
self._game_config.set_game_version(version)
|
||||||
|
self._game_config.save()
|
||||||
|
|
||||||
|
async def download_full_game(self, pre_download=False):
|
||||||
|
game = await self._get_game(pre_download)
|
||||||
|
archive_name = game.latest.path.split("/")[-1]
|
||||||
|
await self._download_file(game.latest.path, archive_name, game.latest.size)
|
||||||
|
|
||||||
|
async def download_full_voicepack(self, language: str, pre_download=False):
|
||||||
|
game = await self._get_game(pre_download)
|
||||||
|
translated_lang = self.voicepack_lang_translate(language)
|
||||||
|
for vo in game.latest.voice_packs:
|
||||||
|
if vo.language == translated_lang:
|
||||||
|
await self._download_file(vo.path, vo.get_name(), vo.size)
|
||||||
|
|
||||||
|
def _extract_game_file(self, archive: str | Path):
|
||||||
|
if isinstance(archive, str):
|
||||||
|
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._game_dir)
|
||||||
|
|
||||||
|
def apply_voicepack(self, archive: str | Path):
|
||||||
|
# Since voicepack packages are unclear about diff package or full package
|
||||||
|
# we will try to extract the voicepack 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._game_dir}")
|
||||||
|
self._extract_game_file(archive)
|
||||||
|
|
||||||
|
async 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._game_dir}")
|
||||||
|
self.delete_game()
|
||||||
|
|
||||||
|
self._game_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._extract_game_file(game_archive)
|
||||||
|
self._version = self.get_current_game_version()
|
||||||
|
self.set_version_config()
|
||||||
|
|
||||||
|
async def download_game_update(self, from_version: str = None, pre_download=False):
|
||||||
|
from_version = from_version if from_version else self.version
|
||||||
|
game = await self._get_game(pre_download=pre_download)
|
||||||
|
if self.version == game.latest.version:
|
||||||
|
raise ValueError("Game is already up to date.")
|
||||||
|
diff_archive = await self.get_game_diff_archive(from_version, pre_download)
|
||||||
|
if diff_archive is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Game diff archive is not available for this version, please reinstall."
|
||||||
|
)
|
||||||
|
await self._download_file(
|
||||||
|
diff_archive.path, diff_archive.name, diff_archive.size
|
||||||
|
)
|
||||||
|
|
||||||
|
async def download_voicepack_update(
|
||||||
|
self, language: str, from_version: str = None, pre_download=False
|
||||||
|
):
|
||||||
|
from_version = from_version if from_version else self.version
|
||||||
|
diff_archive = await self.get_voicepack_diff_archive(
|
||||||
|
language, from_version, pre_download
|
||||||
|
)
|
||||||
|
if diff_archive is None:
|
||||||
|
raise ValueError("Voiceover diff archive is not available for this version")
|
||||||
|
await self._download_file(
|
||||||
|
diff_archive.path, diff_archive.name, diff_archive.size
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_voicepack_diff_archive(
|
||||||
|
self, lang: str, from_version: str = None, pre_download=False
|
||||||
|
) -> installer.Diff:
|
||||||
|
"""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.
|
||||||
|
"""
|
||||||
|
from_version = from_version if from_version else self.version
|
||||||
|
game = await self._get_game(pre_download=pre_download)
|
||||||
|
translated_lang = self.voicepack_lang_translate(lang)
|
||||||
|
for v in game.diffs:
|
||||||
|
if v.version != from_version:
|
||||||
|
continue
|
||||||
|
for vo in v.voice_packs:
|
||||||
|
if vo.language == translated_lang:
|
||||||
|
return vo
|
||||||
|
|
||||||
|
async def get_game_diff_archive(
|
||||||
|
self, from_version: str = None, pre_download=False
|
||||||
|
) -> installer.Diff:
|
||||||
|
"""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.
|
||||||
|
"""
|
||||||
|
from_version = from_version if from_version else self.version
|
||||||
|
game = await self._get_game(pre_download=pre_download)
|
||||||
|
for v in game.diffs:
|
||||||
|
if v.version == from_version:
|
||||||
|
return v
|
||||||
|
|
||||||
|
async def verify_game(self, pkg_version: str | Path = None, ignore_mismatch=None):
|
||||||
if pkg_version is None:
|
if pkg_version is None:
|
||||||
pkg_version = self._game_dir.joinpath("pkg_version")
|
pkg_version = self._game_dir.joinpath("pkg_version")
|
||||||
return await self.verify_from_pkg_version(pkg_version, ignore_mismatch)
|
if ignore_mismatch is None:
|
||||||
|
ignore_mismatch = True
|
||||||
|
return await self._verify_from_pkg_version(pkg_version, ignore_mismatch)
|
||||||
|
|
||||||
async def clear_cache(self):
|
def delete_game(self):
|
||||||
await asyncio.to_thread(shutil.rmtree, self.temp_path, ignore_errors=True)
|
shutil.rmtree(self._game_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
def clear_cache(self):
|
||||||
|
shutil.rmtree(self._cache_path, ignore_errors=True)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import zipfile
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from worthless import constants, helper
|
from worthless import constants, helper
|
||||||
|
@ -9,7 +11,10 @@ from worthless import constants, helper
|
||||||
class HDiffPatch:
|
class HDiffPatch:
|
||||||
"""
|
"""
|
||||||
Contains legacy HDiffPatch support for worthless
|
Contains legacy HDiffPatch support for worthless
|
||||||
|
|
||||||
|
You should not use this class directly, since it's automatically used by GameManager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, git_url=None, data_dir=None):
|
def __init__(self, git_url=None, data_dir=None):
|
||||||
if not git_url:
|
if not git_url:
|
||||||
git_url = constants.HDIFFPATCH_GIT_URL
|
git_url = constants.HDIFFPATCH_GIT_URL
|
||||||
|
@ -97,11 +102,11 @@ class HDiffPatch:
|
||||||
rsp.raise_for_status()
|
rsp.raise_for_status()
|
||||||
for asset in (await rsp.json())["assets"]:
|
for asset in (await rsp.json())["assets"]:
|
||||||
if (
|
if (
|
||||||
asset["name"].endswith(".zip")
|
asset["name"].endswith(".zip")
|
||||||
and "linux" not in asset["name"]
|
and "linux" not in asset["name"]
|
||||||
and "windows" not in asset["name"]
|
and "windows" not in asset["name"]
|
||||||
and "macos" not in asset["name"]
|
and "macos" not in asset["name"]
|
||||||
and "android" not in asset["name"]
|
and "android" not in asset["name"]
|
||||||
):
|
):
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import aiohttp
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
logger = logging.getLogger("worthless.GameManager")
|
logger = logging.getLogger("worthless.helper")
|
||||||
|
|
||||||
|
|
||||||
async def download_file(
|
async def download_file(
|
||||||
|
@ -44,10 +44,10 @@ async def download_file(
|
||||||
threaded: bool = None,
|
threaded: bool = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
headers = {"Range": f"bytes={from_bytes}-{to_bytes if to_bytes else ''}"}
|
headers = {"Range": f"bytes={from_bytes}-{to_bytes if to_bytes else ''}"}
|
||||||
if not threaded:
|
if threaded:
|
||||||
p = file_path
|
|
||||||
else:
|
|
||||||
p = file_path.parent.joinpath(secrets.token_urlsafe(16))
|
p = file_path.parent.joinpath(secrets.token_urlsafe(16))
|
||||||
|
else:
|
||||||
|
p = file_path
|
||||||
p.touch(exist_ok=True)
|
p.touch(exist_ok=True)
|
||||||
rsp = await session.get(file_url, headers=headers, timeout=None)
|
rsp = await session.get(file_url, headers=headers, timeout=None)
|
||||||
if rsp.status == 416:
|
if rsp.status == 416:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import locale
|
import locale
|
||||||
from worthless import constants
|
from worthless import constants
|
||||||
from pathlib import Path
|
|
||||||
from worthless.classes import launcher, installer
|
from worthless.classes import launcher, installer
|
||||||
from worthless.enums import GameVariant
|
from worthless.enums import GameVariant
|
||||||
|
|
||||||
|
@ -28,15 +27,10 @@ class Launcher:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
game_dir: str | Path = None,
|
|
||||||
language: str = None,
|
language: str = None,
|
||||||
variant: GameVariant = None,
|
variant: GameVariant = None,
|
||||||
):
|
):
|
||||||
"""Initialize the launcher API
|
"""Initialize the launcher API"""
|
||||||
|
|
||||||
Args:
|
|
||||||
game_dir (Path): Path to the game directory.
|
|
||||||
"""
|
|
||||||
if not variant:
|
if not variant:
|
||||||
variant = GameVariant.INTERNATIONAL
|
variant = GameVariant.INTERNATIONAL
|
||||||
self._variant = variant
|
self._variant = variant
|
||||||
|
@ -63,10 +57,15 @@ class Launcher:
|
||||||
"zh-cn" # Use chinese language because this is chinese version
|
"zh-cn" # Use chinese language because this is chinese version
|
||||||
)
|
)
|
||||||
case GameVariant.BILIBILI:
|
case GameVariant.BILIBILI:
|
||||||
raise NotImplementedError()
|
self._api = constants.LAUNCHER_API_URL_CN
|
||||||
if not isinstance(game_dir, Path):
|
self._params = {
|
||||||
game_dir = Path(game_dir)
|
"key": "KAtdSsoQ",
|
||||||
self._game_dir = game_dir.resolve()
|
"launcher_id": "17",
|
||||||
|
"channel_id": "14",
|
||||||
|
}
|
||||||
|
self._lang = (
|
||||||
|
"zh-cn" # Use chinese language because this is chinese version
|
||||||
|
)
|
||||||
self._session = aiohttp.ClientSession()
|
self._session = aiohttp.ClientSession()
|
||||||
|
|
||||||
async def _get(self, url, **kwargs) -> dict:
|
async def _get(self, url, **kwargs) -> dict:
|
||||||
|
@ -83,10 +82,6 @@ class Launcher:
|
||||||
)
|
)
|
||||||
return rsp_json
|
return rsp_json
|
||||||
|
|
||||||
@property
|
|
||||||
def game_dir(self):
|
|
||||||
return self._game_dir
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lang(self):
|
def lang(self):
|
||||||
return self._lang
|
return self._lang
|
||||||
|
@ -100,19 +95,6 @@ class Launcher:
|
||||||
lc_info = launcher.Info.from_dict(rsp["data"])
|
lc_info = launcher.Info.from_dict(rsp["data"])
|
||||||
return lc_info
|
return lc_info
|
||||||
|
|
||||||
@game_dir.setter
|
|
||||||
def game_dir(self, game_dir: str | Path) -> None:
|
|
||||||
"""Overrides game directory with another directory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
game_dir (str): New directory to override with.
|
|
||||||
"""
|
|
||||||
if not isinstance(game_dir, Path):
|
|
||||||
game_dir = Path(game_dir)
|
|
||||||
if not game_dir.is_dir():
|
|
||||||
game_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
self._game_dir = game_dir
|
|
||||||
|
|
||||||
@lang.setter
|
@lang.setter
|
||||||
def lang(self, language: str) -> None:
|
def lang(self, language: str) -> None:
|
||||||
"""Overrides system detected language with another language.
|
"""Overrides system detected language with another language.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from worthless.enums import GameVariant
|
||||||
from aiopath import AsyncPath
|
|
||||||
|
|
||||||
|
|
||||||
class LauncherConfig:
|
class LauncherConfig:
|
||||||
|
@ -10,42 +10,72 @@ class LauncherConfig:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_config(game_version, overseas=True):
|
def create_config(game_version, variant: GameVariant):
|
||||||
"""
|
"""
|
||||||
Creates config.ini
|
Creates `config.ini`
|
||||||
|
https://notabug.org/Krock/dawn/src/master/updater/update_gi.sh#L212
|
||||||
"""
|
"""
|
||||||
sub_channel = "0" if overseas else "1"
|
sub_channel = 0
|
||||||
|
channel = 1
|
||||||
|
cps = "mihoyo"
|
||||||
|
match variant:
|
||||||
|
case GameVariant.INTERNATIONAL:
|
||||||
|
channel = 1
|
||||||
|
sub_channel = 0
|
||||||
|
cps = "mihoyo"
|
||||||
|
case GameVariant.CHINESE:
|
||||||
|
channel = 1
|
||||||
|
sub_channel = 1
|
||||||
|
cps = "mihoyo"
|
||||||
|
case GameVariant.BILIBILI:
|
||||||
|
channel = 14
|
||||||
|
sub_channel = 0
|
||||||
|
cps = "bilibili"
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
config.add_section("General")
|
config.add_section("General")
|
||||||
config.set("General", "channel", "1")
|
config.set("General", "channel", str(channel))
|
||||||
config.set("General", "cps", "mihoyo")
|
config.set("General", "cps", cps)
|
||||||
config.set("General", "game_version", game_version)
|
config.set("General", "game_version", game_version)
|
||||||
config.set("General", "sdk_version", "")
|
config.set("General", "sdk_version", "")
|
||||||
config.set("General", "sub_channel", sub_channel)
|
config.set("General", "sub_channel", str(sub_channel))
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def __init__(self, config_path, game_version=None, overseas=True):
|
def __init__(
|
||||||
if isinstance(config_path, str | AsyncPath):
|
self, config_path: PathLike, game_version=None, variant: GameVariant = None
|
||||||
|
):
|
||||||
|
if not variant:
|
||||||
|
variant = GameVariant.INTERNATIONAL
|
||||||
|
if not isinstance(config_path, Path):
|
||||||
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
|
||||||
self.config = ConfigParser()
|
self._config = ConfigParser()
|
||||||
if self.config_path.exists():
|
if self._config_path.exists():
|
||||||
self.config.read(self.config_path)
|
self._config.read(self._config_path)
|
||||||
else:
|
else:
|
||||||
self.config = self.create_config(game_version, overseas)
|
self._config = self.create_config(game_version, variant)
|
||||||
|
|
||||||
def set_game_version(self, game_version):
|
def set_game_version(self, game_version):
|
||||||
self.config.set("General", "game_version", game_version)
|
self._config.set("General", "game_version", game_version)
|
||||||
|
|
||||||
def set_overseas(self, overseas=True):
|
def set_variant(self, variant: GameVariant):
|
||||||
sub_channel = "0" if overseas else "1"
|
sub_channel = 0
|
||||||
self.config.set("General", "sub_channel", sub_channel)
|
match variant:
|
||||||
|
case GameVariant.INTERNATIONAL:
|
||||||
|
sub_channel = 0
|
||||||
|
case GameVariant.CHINESE:
|
||||||
|
sub_channel = 1
|
||||||
|
case GameVariant.BILIBILI:
|
||||||
|
sub_channel = 0
|
||||||
|
self._config.set("General", "sub_channel", str(sub_channel))
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""
|
"""
|
||||||
Saves config.ini
|
Saves config.ini
|
||||||
"""
|
"""
|
||||||
with self.config_path.open("w") as config_file:
|
with self._config_path.open("w") as config_file:
|
||||||
self.config.write(config_file)
|
self._config.write(config_file)
|
||||||
|
|
||||||
|
def get(self, section: str, option: str, **kwargs) -> str:
|
||||||
|
return self._config.get(section=section, option=option, **kwargs)
|
||||||
|
|
|
@ -34,7 +34,7 @@ class Patcher:
|
||||||
data_dir: str | Path | AsyncPath = None,
|
data_dir: str | Path | AsyncPath = None,
|
||||||
patch_url: str = None,
|
patch_url: str = None,
|
||||||
overseas=True,
|
overseas=True,
|
||||||
patch_provider="y0soro",
|
patch_provider="Krock",
|
||||||
):
|
):
|
||||||
if isinstance(gamedir, str | Path):
|
if isinstance(gamedir, str | Path):
|
||||||
gamedir = AsyncPath(gamedir)
|
gamedir = AsyncPath(gamedir)
|
||||||
|
@ -43,9 +43,7 @@ class Patcher:
|
||||||
patch_url = constants.PATCH_LIST[patch_provider].replace(
|
patch_url = constants.PATCH_LIST[patch_provider].replace(
|
||||||
"http://", "https://"
|
"http://", "https://"
|
||||||
)
|
)
|
||||||
self._patch_url = (patch_url if patch_url else constants.PATCH_GIT_URL).replace(
|
self._patch_url = patch_url
|
||||||
"http://", "https://"
|
|
||||||
)
|
|
||||||
if not data_dir:
|
if not data_dir:
|
||||||
self._appdirs = constants.APPDIRS
|
self._appdirs = constants.APPDIRS
|
||||||
self._patch_path = AsyncPath(self._appdirs.user_data_dir).joinpath("Patch")
|
self._patch_path = AsyncPath(self._appdirs.user_data_dir).joinpath("Patch")
|
||||||
|
|
Loading…
Reference in New Issue