Compare commits

..

6 Commits

11 changed files with 186 additions and 19 deletions

41
poetry.lock generated
View File

@ -143,6 +143,20 @@ files = [
docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
[[package]]
name = "fire"
version = "0.5.0"
description = "A library for automatically generating command line interfaces."
optional = false
python-versions = "*"
files = [
{file = "fire-0.5.0.tar.gz", hash = "sha256:a6b0d49e98c8963910021f92bba66f65ab440da2982b78eb1bbf95a0a34aacc6"},
]
[package.dependencies]
six = "*"
termcolor = "*"
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.5.24" version = "2.5.24"
@ -358,6 +372,31 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-g
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "termcolor"
version = "2.3.0"
description = "ANSI color formatting for output in terminal"
optional = false
python-versions = ">=3.7"
files = [
{file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"},
{file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"},
]
[package.extras]
tests = ["pytest", "pytest-cov"]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.0.3" version = "2.0.3"
@ -398,4 +437,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "7d2bf3678e910680af7a343064533ccd713d5583786eef1d7d8fa2d930da6128" content-hash = "a96832cd8f35bc83d3041fd4ec46972877becf824e5a307207a48459260b5289"

View File

@ -15,6 +15,10 @@ requests = "^2.31.0"
pytest = "^7.3.1" pytest = "^7.3.1"
pre-commit = "^3.3.3" pre-commit = "^3.3.3"
[tool.poetry.group.cli.dependencies]
fire = "^0.5.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

View File

@ -0,0 +1 @@
__version__ = "0.1.0"

5
vollerei/__main__.py Normal file
View File

@ -0,0 +1,5 @@
from vollerei.cli import CLI
from fire import Fire
Fire(CLI, name="vollerei")

View File

@ -34,3 +34,11 @@ class PatcherABC(ABC):
unpatching fails then it'll raise `UnpatchingFailedError`. unpatching fails then it'll raise `UnpatchingFailedError`.
""" """
pass pass
@abstractmethod
def check_telemetry(self):
pass
@abstractmethod
def block_telemetry(self):
pass

View File

@ -1,6 +1,22 @@
from typing import Any from pathlib import Path
from vollerei import __version__
from vollerei.cli.hsr import HSR
from vollerei.hsr import PatchType
class CLI: class CLI:
def __init__(self): def __init__(self, game_path: str = None, patch_type=None) -> None:
pass """
Vollerei CLI
"""
print(f"Vollerei v{__version__}")
if not game_path:
game_path = Path.cwd()
game_path = Path(game_path)
if patch_type is None:
patch_type = PatchType.Jadeite
elif isinstance(patch_type, str):
patch_type = PatchType[patch_type]
elif isinstance(patch_type, int):
patch_type = PatchType(patch_type)
self.hsr = HSR(game_path=game_path, patch_type=patch_type)

51
vollerei/cli/hsr.py Normal file
View File

@ -0,0 +1,51 @@
from traceback import print_exc
from vollerei.hsr import Game, Patcher
from vollerei.exceptions.patcher import PatcherError, PatchUpdateError
from vollerei.hsr.patcher import PatchType
class HSR:
def __init__(
self, game_path=None, patch_type: PatchType = PatchType.Jadeite
) -> None:
self._game = Game(game_path)
print("Game directory:", self._game.path)
print("Game version:", self._game.get_version_str())
self._patcher = Patcher()
self._patcher.patch_type = patch_type
# Double _ means private to prevent Fire from invoking it
def patch_type(self):
print("Patch type:", self._patcher.patch_type.name)
def __update_patch(self):
self.patch_type()
print("Updating patch...", end=" ")
try:
self._patcher.update_patch()
except PatchUpdateError as e:
print("FAILED")
print(f"Patch update failed with following error: {e} ({e.__context__})")
print_exc()
return False
print("OK")
return True
def update_patch(self):
self.__update_patch()
def patch(self):
if not self.__update_patch():
return
try:
print("Patching game...", end=" ")
jadelte_dir = self._patcher.patch_game(game=self._game)
except PatcherError as e:
print("FAILED")
print(f"Patching failed with following error: {e}")
return
print("OK")
print("Jadelte executable is located at:", jadelte_dir.joinpath("jadelte.exe"))
print(
"Patching succeeded, but note that you need to run the game using Jadelte to use the patch."
)

View File

@ -1,6 +1,6 @@
# Re-exports # Re-exports
from vollerei.hsr.patcher import Patcher from vollerei.hsr.patcher import Patcher, PatchType
from vollerei.hsr.launcher import Game, GameChannel from vollerei.hsr.launcher import Game, GameChannel
__all__ = ["Patcher", "Game", "GameChannel"] __all__ = ["Patcher", "PatchType", "Game", "GameChannel"]

View File

@ -56,6 +56,10 @@ class Game(GameABC):
tuple[int, int, int]: The version as a tuple of integers. tuple[int, int, int]: The version as a tuple of integers.
""" """
data_file = self.data_folder().joinpath("data.unity3d")
if not data_file.exists():
return (0, 0, 0)
def bytes_to_int(byte_array: list[bytes]) -> int: def bytes_to_int(byte_array: list[bytes]) -> int:
bytes_as_int = int.from_bytes(byte_array, byteorder="big") bytes_as_int = int.from_bytes(byte_array, byteorder="big")
actual_int = bytes_as_int - 48 # 48 is the ASCII code for 0 actual_int = bytes_as_int - 48 # 48 is the ASCII code for 0

View File

@ -3,7 +3,11 @@ from shutil import copy2
from distutils.version import StrictVersion 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,
PatcherError,
PatchUpdateError,
)
from vollerei.hsr.launcher.game import Game, GameChannel from vollerei.hsr.launcher.game import Game, GameChannel
from vollerei.utils import download_and_extract, Git, Xdelta3 from vollerei.utils import download_and_extract, Git, Xdelta3
from vollerei.paths import tools_data_path from vollerei.paths import tools_data_path
@ -63,11 +67,14 @@ class Patcher(PatcherABC):
f.write(file_version) f.write(file_version)
def update_patch(self): def update_patch(self):
try:
match self._patch_type: match self._patch_type:
case PatchType.Astra: case PatchType.Astra:
self._update_astra() self._update_astra()
case PatchType.Jadeite: case PatchType.Jadeite:
self._update_jadeite() self._update_jadeite()
except Exception as e:
raise PatchUpdateError("Failed to update patch.") from e
def _patch_astra(self, game: Game): def _patch_astra(self, game: Game):
if game.get_version() != (1, 0, 5): if game.get_version() != (1, 0, 5):
@ -119,7 +126,7 @@ class Patcher(PatcherABC):
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 PatcherError(GameNotInstalledError("Game is not installed"))
match self._patch_type: match self._patch_type:
case PatchType.Astra: case PatchType.Astra:
self._patch_astra(game) self._patch_astra(game)
@ -128,3 +135,9 @@ class Patcher(PatcherABC):
def unpatch_game(self, game: Game): def unpatch_game(self, game: Game):
pass pass
def check_telemetry(self):
pass
def block_telemetry(self):
pass

View File

@ -1,5 +1,8 @@
import errno
from io import BytesIO from io import BytesIO
import os
import subprocess import subprocess
import stat
from zipfile import ZipFile from zipfile import ZipFile
import requests import requests
import json import json
@ -145,16 +148,39 @@ class Git:
rmtree(path) rmtree(path)
try: try:
if not path_as_path.exists(): if not path_as_path.exists():
raise subprocess.CalledProcessError raise subprocess.CalledProcessError(0, cmd="Vollerei-generated error")
origin_url = subprocess.check_output( origin_url = (
subprocess.check_output(
["git", "config", "--get", "remote.origin.url"], cwd=path ["git", "config", "--get", "remote.origin.url"], cwd=path
).decode() )
.decode()
.strip()
)
if origin_url != url: if origin_url != url:
raise subprocess.CalledProcessError raise subprocess.CalledProcessError(0, cmd="Vollerei-generated error")
subprocess.check_call(["git", "pull"], cwd=path) subprocess.check_call(["git", "pull"], cwd=path)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
if path_as_path.exists(): if path_as_path.exists():
rmtree(path) try:
def handle_error(func, path, exc):
excvalue = exc[1]
if (
func in (os.rmdir, os.remove)
and excvalue.errno == errno.EACCES
):
os.chmod(
path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
) # 0777
func(path)
else:
raise
rmtree(path, ignore_errors=False, onerror=handle_error)
except OSError as e:
raise GitCloneError(
f"Failed to delete existing repository {path_as_path}"
) from e
try: try:
subprocess.check_call(["git", "clone", url, path]) subprocess.check_call(["git", "clone", url, path])
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e: