Compare commits

...

2 Commits

Author SHA1 Message Date
7277d78472 feat: complete the patdher for star rail 2023-06-17 02:31:26 +07:00
c483f289c9 chore: add return type 2023-06-16 20:11:58 +07:00
8 changed files with 184 additions and 35 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 # Common
telemetry_hosts = [ telemetry_hosts = [
# Global # Global
@ -14,13 +12,3 @@ telemetry_hosts = [
# HSR # HSR
astra_repo = "https://notabug.org/mkrsym1/astra" astra_repo = "https://notabug.org/mkrsym1/astra"
jadeite_repo = "https://codeberg.org/mkrsym1/jadeite/" 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 os import PathLike
from pathlib import Path from pathlib import Path
from enum import Enum
from vollerei.abc.launcher.game import GameABC from vollerei.abc.launcher.game import GameABC
from vollerei.hsr.constants import md5sums
class GameChannel(Enum):
Overseas = 0
China = 1
class Game(GameABC): class Game(GameABC):
def __init__(self, path: PathLike = None): 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: def is_installed(self) -> bool:
if self.path is None: if self._path is None:
return False return False
if ( if (
not self.path.joinpath("StarRail.exe").exists() not self._path.joinpath("StarRail.exe").exists()
or not self.path.joinpath("StarRailBase.dll").exists() or not self._path.joinpath("StarRailBase.dll").exists()
): ):
return False 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.abc.patcher import PatcherABC
from vollerei.exceptions.game import GameNotInstalledError from vollerei.exceptions.game import GameNotInstalledError
from vollerei.exceptions.patcher import VersionNotSupportedError from vollerei.exceptions.patcher import VersionNotSupportedError
from vollerei.hsr.launcher.game import Game from vollerei.hsr.launcher.game import Game, GameChannel
from vollerei.utils.git import Git from vollerei.utils import download_and_extract, Git, Xdelta3
from vollerei.constants import tools_data_path, astra_repo, jadeite_repo from vollerei.paths import tools_data_path
from enum import Enum from vollerei.constants import astra_repo, jadeite_repo
class PatchType(Enum): class PatchType(Enum):
@ -23,7 +27,11 @@ class Patcher(PatcherABC):
def __init__(self, patch_type: PatchType = PatchType.Jadeite): def __init__(self, patch_type: PatchType = PatchType.Jadeite):
self._patch_type: PatchType = patch_type self._patch_type: PatchType = patch_type
self._path = tools_data_path.joinpath("patcher") 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._git = Git()
self._xdelta3 = Xdelta3()
@property @property
def patch_type(self) -> PatchType: def patch_type(self) -> PatchType:
@ -34,10 +42,21 @@ class Patcher(PatcherABC):
self._patch_type = value self._patch_type = value
def _update_astra(self): 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): 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): def update_patch(self):
match self._patch_type: match self._patch_type:
@ -51,10 +70,54 @@ class Patcher(PatcherABC):
raise VersionNotSupportedError( raise VersionNotSupportedError(
"Only version 1.0.5 is supported by Astra patch." "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): def _patch_jadeite(self):
pass """
"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): def patch_game(self, game: Game):
if not game.is_installed(): if not game.is_installed():
raise GameNotInstalledError("Game is not 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 requests
import json import json
from pathlib import Path from pathlib import Path
from zipfile import ZipFile
from io import BytesIO
from shutil import which, rmtree from shutil import which, rmtree
from urllib.parse import urlparse from urllib.parse import urlparse
from vollerei.constants import utils_cache_path from vollerei.constants import utils_cache_path
from vollerei.utils.git.exceptions import GitCloneError from vollerei.utils.git.exceptions import GitCloneError
from vollerei.utils import download_and_extract
class Git: class Git:
@ -22,7 +21,7 @@ class Git:
self._cache.mkdir(parents=True, exist_ok=True) self._cache.mkdir(parents=True, exist_ok=True)
@staticmethod @staticmethod
def is_installed(): def is_installed() -> bool:
""" """
Check for git installation Check for git installation
@ -62,14 +61,7 @@ class Git:
return data[0]["sha"] return data[0]["sha"]
def _download_and_extract_zip(self, url: str, path: Path) -> None: def _download_and_extract_zip(self, url: str, path: Path) -> None:
rsp = requests.get(url, stream=True) download_and_extract(url, path)
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)
path.joinpath(".git/PLEASE_INSTALL_GIT").touch() path.joinpath(".git/PLEASE_INSTALL_GIT").touch()
def _clone(self, url: str, path: str = None) -> None: def _clone(self, url: str, path: str = None) -> None:
@ -99,6 +91,23 @@ class Git:
else: else:
raise NotImplementedError 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: def pull_or_clone(self, url: str, path: str = None) -> None:
""" """
Pulls or clones a git repository Pulls or clones a git repository