feat: complete the patdher for star rail

This commit is contained in:
tretrauit 2023-06-17 02:31:26 +07:00
parent c483f289c9
commit 7277d78472
8 changed files with 183 additions and 34 deletions

6
vollerei/cli/__init__.py Normal file
View File

@ -0,0 +1,6 @@
from typing import Any
class CLI:
def __init__(self):
pass

View File

@ -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
View 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",
},
}
}

View File

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

View File

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

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

View File

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