feat: complete the patdher for star rail
This commit is contained in:
parent
c483f289c9
commit
7277d78472
6
vollerei/cli/__init__.py
Normal file
6
vollerei/cli/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
class CLI:
|
||||
def __init__(self):
|
||||
pass
|
@ -1,5 +1,3 @@
|
||||
from platformdirs import PlatformDirs
|
||||
|
||||
# Common
|
||||
telemetry_hosts = [
|
||||
# Global
|
||||
@ -14,13 +12,3 @@ telemetry_hosts = [
|
||||
# HSR
|
||||
astra_repo = "https://notabug.org/mkrsym1/astra"
|
||||
jadeite_repo = "https://codeberg.org/mkrsym1/jadeite/"
|
||||
hsr_latest_version = (1, 1, 0)
|
||||
|
||||
base_dirs = PlatformDirs("vollerei", "tretrauit", roaming=True)
|
||||
tools_data_path = base_dirs.site_data_path.joinpath("tools")
|
||||
tools_cache_path = base_dirs.site_cache_path.joinpath("tools")
|
||||
tools_cache_path.mkdir(parents=True, exist_ok=True)
|
||||
launcher_cache_path = base_dirs.site_cache_path.joinpath("launcher")
|
||||
launcher_cache_path.mkdir(parents=True, exist_ok=True)
|
||||
utils_cache_path = base_dirs.site_cache_path.joinpath("utils")
|
||||
utils_cache_path.mkdir(parents=True, exist_ok=True)
|
||||
|
13
vollerei/hsr/constants.py
Normal file
13
vollerei/hsr/constants.py
Normal file
@ -0,0 +1,13 @@
|
||||
latest_version = (1, 1, 0)
|
||||
md5sums = {
|
||||
"1.0.5": {
|
||||
"cn": {
|
||||
"StarRailBase.dll": "66c42871ce82456967d004ccb2d7cf77",
|
||||
"UnityPlayer.dll": "0c866c44bb3752031a8c12ffe935b26f",
|
||||
},
|
||||
"os": {
|
||||
"StarRailBase.dll": "8aa3790aafa3dd176678392f3f93f435",
|
||||
"UnityPlayer.dll": "f17b9b7f9b8c9cbd211bdff7771a80c2",
|
||||
},
|
||||
}
|
||||
}
|
@ -1,17 +1,44 @@
|
||||
from hashlib import md5
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from vollerei.abc.launcher.game import GameABC
|
||||
from vollerei.hsr.constants import md5sums
|
||||
|
||||
|
||||
class GameChannel(Enum):
|
||||
Overseas = 0
|
||||
China = 1
|
||||
|
||||
|
||||
class Game(GameABC):
|
||||
def __init__(self, path: PathLike = None):
|
||||
self.path: Path | None = Path(path) if path else None
|
||||
self._path: Path | None = Path(path) if path else None
|
||||
|
||||
@property
|
||||
def path(self) -> Path | None:
|
||||
return self._path
|
||||
|
||||
def is_installed(self) -> bool:
|
||||
if self.path is None:
|
||||
if self._path is None:
|
||||
return False
|
||||
if (
|
||||
not self.path.joinpath("StarRail.exe").exists()
|
||||
or not self.path.joinpath("StarRailBase.dll").exists()
|
||||
not self._path.joinpath("StarRail.exe").exists()
|
||||
or not self._path.joinpath("StarRailBase.dll").exists()
|
||||
):
|
||||
return False
|
||||
|
||||
def get_channel(self) -> GameChannel:
|
||||
if self.get_version() == (1, 0, 5):
|
||||
for channel, v in md5sums["1.0.5"].values():
|
||||
for file, md5sum in v.values():
|
||||
if (
|
||||
md5(self._path.joinpath(file).read_bytes()).hexdigest()
|
||||
!= md5sum
|
||||
):
|
||||
continue
|
||||
match channel:
|
||||
case "cn":
|
||||
return GameChannel.China
|
||||
case "os":
|
||||
return GameChannel.Overseas
|
||||
|
@ -1,10 +1,14 @@
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from shutil import copy2
|
||||
from distutils.version import StrictVersion
|
||||
from vollerei.abc.patcher import PatcherABC
|
||||
from vollerei.exceptions.game import GameNotInstalledError
|
||||
from vollerei.exceptions.patcher import VersionNotSupportedError
|
||||
from vollerei.hsr.launcher.game import Game
|
||||
from vollerei.utils.git import Git
|
||||
from vollerei.constants import tools_data_path, astra_repo, jadeite_repo
|
||||
from enum import Enum
|
||||
from vollerei.hsr.launcher.game import Game, GameChannel
|
||||
from vollerei.utils import download_and_extract, Git, Xdelta3
|
||||
from vollerei.paths import tools_data_path
|
||||
from vollerei.constants import astra_repo, jadeite_repo
|
||||
|
||||
|
||||
class PatchType(Enum):
|
||||
@ -23,7 +27,11 @@ class Patcher(PatcherABC):
|
||||
def __init__(self, patch_type: PatchType = PatchType.Jadeite):
|
||||
self._patch_type: PatchType = patch_type
|
||||
self._path = tools_data_path.joinpath("patcher")
|
||||
self._path.mkdir(parents=True, exist_ok=True)
|
||||
self._jadeite = self._path.joinpath("jadeite")
|
||||
self._astra = self._path.joinpath("astra")
|
||||
self._git = Git()
|
||||
self._xdelta3 = Xdelta3()
|
||||
|
||||
@property
|
||||
def patch_type(self) -> PatchType:
|
||||
@ -34,10 +42,21 @@ class Patcher(PatcherABC):
|
||||
self._patch_type = value
|
||||
|
||||
def _update_astra(self):
|
||||
self._git.pull_or_clone(astra_repo, self._path.joinpath("astra"))
|
||||
self._git.pull_or_clone(astra_repo, self._astra)
|
||||
|
||||
def _update_jadeite(self):
|
||||
self._git.pull_or_clone(jadeite_repo, self._path.joinpath("jadeite"))
|
||||
file = self._git.get_latest_release_dl(jadeite_repo)[0]
|
||||
file_version = Path(file).stem[1:]
|
||||
current_version = None
|
||||
if self._jadeite.joinpath("version").exists():
|
||||
with open(self._jadeite.joinpath("version"), "r") as f:
|
||||
current_version = f.read()
|
||||
if current_version:
|
||||
if StrictVersion(file_version) <= StrictVersion(current_version):
|
||||
return
|
||||
download_and_extract(file, self._jadeite)
|
||||
with open(self._jadeite.joinpath("version"), "w") as f:
|
||||
f.write(file_version)
|
||||
|
||||
def update_patch(self):
|
||||
match self._patch_type:
|
||||
@ -51,10 +70,54 @@ class Patcher(PatcherABC):
|
||||
raise VersionNotSupportedError(
|
||||
"Only version 1.0.5 is supported by Astra patch."
|
||||
)
|
||||
self._update_astra()
|
||||
file_type = None
|
||||
match game.get_channel():
|
||||
case GameChannel.China:
|
||||
file_type = "cn"
|
||||
case GameChannel.Overseas:
|
||||
file_type = "os"
|
||||
# Backup
|
||||
for file in ["UnityPlayer.dll", "StarRailBase.dll"]:
|
||||
game.path.joinpath(file).rename(game.path.joinpath(f"{file}.bak"))
|
||||
# Patch
|
||||
for file in ["UnityPlayer.dll", "StarRailBase.dll"]:
|
||||
self._xdelta3.patch_file(
|
||||
self._astra.joinpath(f"{file_type}/diffs/{file}.vcdiff"),
|
||||
game.path.joinpath(f"{file}.bak"),
|
||||
game.path.joinpath(file),
|
||||
)
|
||||
# Copy files
|
||||
for file in self._astra.joinpath(f"{file_type}/files/").rglob("*"):
|
||||
if file.suffix == ".bat":
|
||||
continue
|
||||
if file.is_dir():
|
||||
game.path.joinpath(
|
||||
file.relative_to(self._astra.joinpath(f"{file_type}/files/"))
|
||||
).mkdir(parents=True, exist_ok=True)
|
||||
copy2(
|
||||
file,
|
||||
game.path.joinpath(
|
||||
file.relative_to(self._astra.joinpath(f"{file_type}/files/"))
|
||||
),
|
||||
)
|
||||
|
||||
def _patch_jadeite(self, game: Game):
|
||||
pass
|
||||
def _patch_jadeite(self):
|
||||
"""
|
||||
"Patch" the game with Jadeite patch.
|
||||
|
||||
Unlike Astra patch, Jadeite patch does not modify the game files directly
|
||||
but uses DLLs to patch the game in memory and it has an injector to do that
|
||||
automatically.
|
||||
"""
|
||||
self._update_jadeite()
|
||||
return self._jadeite
|
||||
|
||||
def patch_game(self, game: Game):
|
||||
if not game.is_installed():
|
||||
raise GameNotInstalledError("Game is not installed")
|
||||
match self._patch_type:
|
||||
case PatchType.Astra:
|
||||
self._patch_astra(game)
|
||||
case PatchType.Jadeite:
|
||||
return self._patch_jadeite()
|
||||
|
21
vollerei/paths.py
Normal file
21
vollerei/paths.py
Normal file
@ -0,0 +1,21 @@
|
||||
from pathlib import Path
|
||||
from platformdirs import PlatformDirs
|
||||
|
||||
|
||||
base_paths = PlatformDirs("vollerei", "tretrauit", roaming=True)
|
||||
tools_data_path: Path = None
|
||||
tools_cache_path: Path = None
|
||||
launcher_cache_path: Path = None
|
||||
utils_cache_path: Path = None
|
||||
|
||||
|
||||
def init_paths():
|
||||
global tools_data_path, tools_cache_path, launcher_cache_path, utils_cache_path
|
||||
tools_data_path = base_paths.site_data_path.joinpath("tools")
|
||||
tools_cache_path = base_paths.site_cache_path.joinpath("tools")
|
||||
launcher_cache_path = base_paths.site_cache_path.joinpath("launcher")
|
||||
utils_cache_path = base_paths.site_cache_path.joinpath("utils")
|
||||
tools_data_path.mkdir(parents=True, exist_ok=True)
|
||||
tools_cache_path.mkdir(parents=True, exist_ok=True)
|
||||
launcher_cache_path.mkdir(parents=True, exist_ok=True)
|
||||
utils_cache_path.mkdir(parents=True, exist_ok=True)
|
22
vollerei/utils/__init__.py
Normal file
22
vollerei/utils/__init__.py
Normal file
@ -0,0 +1,22 @@
|
||||
import requests
|
||||
from zipfile import ZipFile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
# Re-exports
|
||||
from vollerei.utils.git import Git
|
||||
from vollerei.utils.xdelta3 import Xdelta3
|
||||
|
||||
|
||||
__all__ = ["Git", "Xdelta3", "download_and_extract"]
|
||||
|
||||
|
||||
def download_and_extract(url: str, path: Path) -> None:
|
||||
rsp = requests.get(url, stream=True)
|
||||
rsp.raise_for_status()
|
||||
with BytesIO() as f:
|
||||
for chunk in rsp.iter_content(chunk_size=32768):
|
||||
f.write(chunk)
|
||||
f.seek(0)
|
||||
with ZipFile(f) as z:
|
||||
z.extractall(path)
|
@ -2,12 +2,11 @@ import subprocess
|
||||
import requests
|
||||
import json
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
from io import BytesIO
|
||||
from shutil import which, rmtree
|
||||
from urllib.parse import urlparse
|
||||
from vollerei.constants import utils_cache_path
|
||||
from vollerei.utils.git.exceptions import GitCloneError
|
||||
from vollerei.utils import download_and_extract
|
||||
|
||||
|
||||
class Git:
|
||||
@ -62,14 +61,7 @@ class Git:
|
||||
return data[0]["sha"]
|
||||
|
||||
def _download_and_extract_zip(self, url: str, path: Path) -> None:
|
||||
rsp = requests.get(url, stream=True)
|
||||
rsp.raise_for_status()
|
||||
file = BytesIO()
|
||||
with open(file, "wb") as f:
|
||||
for chunk in rsp.iter_content(chunk_size=32768):
|
||||
f.write(chunk)
|
||||
zip_file = ZipFile(file)
|
||||
zip_file.extractall(path)
|
||||
download_and_extract(url, path)
|
||||
path.joinpath(".git/PLEASE_INSTALL_GIT").touch()
|
||||
|
||||
def _clone(self, url: str, path: str = None) -> None:
|
||||
@ -99,6 +91,23 @@ class Git:
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_latest_release_dl(self, url: str) -> list[str]:
|
||||
dl = []
|
||||
if Path(url).suffix == ".git":
|
||||
url = url[:-4]
|
||||
url_info = urlparse(url)
|
||||
netloc = url_info.netloc
|
||||
if self._is_gitea(netloc):
|
||||
rsp = requests.get(
|
||||
f"https://{netloc}/api/v1/repos/{url_info.path}/releases/latest",
|
||||
)
|
||||
rsp.raise_for_status()
|
||||
data = rsp.json()
|
||||
for asset in data["assets"]:
|
||||
dl.append(asset["browser_download_url"])
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def pull_or_clone(self, url: str, path: str = None) -> None:
|
||||
"""
|
||||
Pulls or clones a git repository
|
||||
|
Loading…
Reference in New Issue
Block a user