Compare commits
2 Commits
9d7322ad1e
...
7277d78472
Author | SHA1 | Date | |
---|---|---|---|
7277d78472 | |||
c483f289c9 |
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
|
# 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
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 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
|
||||||
|
@ -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
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 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
|
||||||
|
Loading…
Reference in New Issue
Block a user