Compare commits
23 Commits
ecd204428d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a084c61cae | |||
| 10d5f3f208 | |||
| 7707a64733 | |||
| 76bccf6a6f | |||
| 874686a95c | |||
| 1863fafd85 | |||
| ac76df577b | |||
| ed641f890d | |||
| 9e64bfc531 | |||
| 25cfcdf0f0 | |||
| b2eec5ee30 | |||
| 7dbe890bf3 | |||
| 976308ac85 | |||
| fba6cecc38 | |||
| f5007b6aa5 | |||
| da9c930d2e | |||
| fc3a43bec7 | |||
| 844453eabc | |||
| d6d1fdee6e | |||
| 7440b6041f | |||
| 75649df729 | |||
| 707bcc14c3 | |||
| 18aa7935cb |
13
.github/workflows/docs.yml
vendored
13
.github/workflows/docs.yml
vendored
@ -9,20 +9,25 @@ jobs:
|
|||||||
# Build job
|
# Build job
|
||||||
build:
|
build:
|
||||||
# Specify runner + build & upload the static files as an artifact
|
# Specify runner + build & upload the static files as an artifact
|
||||||
runs-on: python@latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
id: checkout-code
|
id: checkout-code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
cache: 'pip' # caching pip dependencies
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: install
|
id: install
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
pipx install poetry --python $(which python)
|
||||||
pip install mkdocs mkdocstrings mkdocs-material
|
poetry install --only docs
|
||||||
- name: Build docs
|
- name: Build docs
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
mkdocs build
|
poetry run mkdocs build
|
||||||
|
|
||||||
- name: Upload static files as artifact
|
- name: Upload static files as artifact
|
||||||
id: deployment
|
id: deployment
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Common function for all games
|
# Function for all games
|
||||||
|
|
||||||
Since you should use the specific implementation for the target game instead, these functions may not be correctly documented.
|
Since you should use the specific implementation for the target game instead, these functions may not be correctly documented.
|
||||||
|
|
||||||
|
|||||||
7
docs/game/launcher/game.md
Normal file
7
docs/game/launcher/game.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Game
|
||||||
|
|
||||||
|
::: vollerei.game.launcher.Game
|
||||||
|
handler: python
|
||||||
|
options:
|
||||||
|
show_root_heading: true
|
||||||
|
show_source: true
|
||||||
73
poetry.lock
generated
73
poetry.lock
generated
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "babel"
|
name = "babel"
|
||||||
@ -6,6 +6,7 @@ version = "2.16.0"
|
|||||||
description = "Internationalization utilities"
|
description = "Internationalization utilities"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"},
|
{file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"},
|
||||||
{file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"},
|
{file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"},
|
||||||
@ -20,6 +21,8 @@ version = "1.1.0"
|
|||||||
description = "Python bindings for the Brotli compression library"
|
description = "Python bindings for the Brotli compression library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "platform_python_implementation == \"CPython\""
|
||||||
files = [
|
files = [
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"},
|
{file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"},
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"},
|
{file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"},
|
||||||
@ -154,6 +157,8 @@ version = "1.1.0.0"
|
|||||||
description = "Python CFFI bindings to the Brotli library"
|
description = "Python CFFI bindings to the Brotli library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "platform_python_implementation == \"PyPy\""
|
||||||
files = [
|
files = [
|
||||||
{file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"},
|
{file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"},
|
||||||
{file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"},
|
{file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"},
|
||||||
@ -193,6 +198,7 @@ version = "2024.2.2"
|
|||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
groups = ["main", "docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
|
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
|
||||||
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
|
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
|
||||||
@ -204,6 +210,8 @@ version = "1.17.1"
|
|||||||
description = "Foreign Function Interface for Python calling C code."
|
description = "Foreign Function Interface for Python calling C code."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "platform_python_implementation == \"PyPy\""
|
||||||
files = [
|
files = [
|
||||||
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||||
@ -283,6 +291,7 @@ version = "3.4.0"
|
|||||||
description = "Validate configuration and produce human readable error messages."
|
description = "Validate configuration and produce human readable error messages."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
|
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
|
||||||
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
||||||
@ -294,6 +303,7 @@ version = "3.3.2"
|
|||||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.0"
|
python-versions = ">=3.7.0"
|
||||||
|
groups = ["main", "docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
||||||
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
|
||||||
@ -393,6 +403,7 @@ version = "2.1.0"
|
|||||||
description = "Cleo allows you to create beautiful and testable command-line interfaces."
|
description = "Cleo allows you to create beautiful and testable command-line interfaces."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7,<4.0"
|
python-versions = ">=3.7,<4.0"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"},
|
{file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"},
|
||||||
{file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"},
|
{file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"},
|
||||||
@ -408,6 +419,7 @@ version = "8.1.7"
|
|||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||||
@ -422,10 +434,12 @@ version = "0.4.6"
|
|||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
groups = ["cli", "dev", "docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
markers = {cli = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crashtest"
|
name = "crashtest"
|
||||||
@ -433,6 +447,7 @@ version = "0.4.1"
|
|||||||
description = "Manage Python errors with ease"
|
description = "Manage Python errors with ease"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7,<4.0"
|
python-versions = ">=3.7,<4.0"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"},
|
{file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"},
|
||||||
{file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"},
|
{file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"},
|
||||||
@ -444,6 +459,7 @@ version = "0.3.8"
|
|||||||
description = "Distribution utilities"
|
description = "Distribution utilities"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
|
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
|
||||||
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
|
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
|
||||||
@ -455,6 +471,7 @@ version = "3.14.0"
|
|||||||
description = "A platform independent file lock."
|
description = "A platform independent file lock."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"},
|
{file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"},
|
||||||
{file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"},
|
{file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"},
|
||||||
@ -471,6 +488,7 @@ version = "2.1.0"
|
|||||||
description = "Copy your docs directly to the gh-pages branch."
|
description = "Copy your docs directly to the gh-pages branch."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
|
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
|
||||||
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
|
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
|
||||||
@ -488,6 +506,7 @@ version = "1.5.1"
|
|||||||
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
|
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "griffe-1.5.1-py3-none-any.whl", hash = "sha256:ad6a7980f8c424c9102160aafa3bcdf799df0e75f7829d75af9ee5aef656f860"},
|
{file = "griffe-1.5.1-py3-none-any.whl", hash = "sha256:ad6a7980f8c424c9102160aafa3bcdf799df0e75f7829d75af9ee5aef656f860"},
|
||||||
{file = "griffe-1.5.1.tar.gz", hash = "sha256:72964f93e08c553257706d6cd2c42d1c172213feb48b2be386f243380b405d4b"},
|
{file = "griffe-1.5.1.tar.gz", hash = "sha256:72964f93e08c553257706d6cd2c42d1c172213feb48b2be386f243380b405d4b"},
|
||||||
@ -502,6 +521,7 @@ version = "2.5.36"
|
|||||||
description = "File identification library for Python"
|
description = "File identification library for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"},
|
{file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"},
|
||||||
{file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"},
|
{file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"},
|
||||||
@ -516,6 +536,7 @@ version = "3.7"
|
|||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
groups = ["main", "docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||||
@ -527,6 +548,7 @@ version = "1.0.0"
|
|||||||
description = "deflate64 compression/decompression library"
|
description = "deflate64 compression/decompression library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "inflate64-1.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a90c0bdf4a7ecddd8a64cc977181810036e35807f56b0bcacee9abb0fcfd18dc"},
|
{file = "inflate64-1.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a90c0bdf4a7ecddd8a64cc977181810036e35807f56b0bcacee9abb0fcfd18dc"},
|
||||||
{file = "inflate64-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57fe7c14aebf1c5a74fc3b70d355be1280a011521a76aa3895486e62454f4242"},
|
{file = "inflate64-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57fe7c14aebf1c5a74fc3b70d355be1280a011521a76aa3895486e62454f4242"},
|
||||||
@ -594,6 +616,7 @@ version = "2.0.0"
|
|||||||
description = "brain-dead simple config-ini parsing"
|
description = "brain-dead simple config-ini parsing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
@ -605,6 +628,7 @@ version = "3.1.4"
|
|||||||
description = "A very fast and expressive template engine."
|
description = "A very fast and expressive template engine."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
||||||
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
||||||
@ -622,6 +646,7 @@ version = "3.7"
|
|||||||
description = "Python implementation of John Gruber's Markdown."
|
description = "Python implementation of John Gruber's Markdown."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"},
|
{file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"},
|
||||||
{file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"},
|
{file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"},
|
||||||
@ -637,6 +662,7 @@ version = "3.0.2"
|
|||||||
description = "Safely add untrusted strings to HTML/XML markup."
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
|
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
|
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
|
||||||
@ -707,6 +733,7 @@ version = "1.3.4"
|
|||||||
description = "A deep merge function for 🐍."
|
description = "A deep merge function for 🐍."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
|
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
|
||||||
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
|
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
|
||||||
@ -718,6 +745,7 @@ version = "1.6.1"
|
|||||||
description = "Project documentation with Markdown."
|
description = "Project documentation with Markdown."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"},
|
{file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"},
|
||||||
{file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"},
|
{file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"},
|
||||||
@ -748,6 +776,7 @@ version = "1.2.0"
|
|||||||
description = "Automatically link across pages in MkDocs."
|
description = "Automatically link across pages in MkDocs."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f"},
|
{file = "mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f"},
|
||||||
{file = "mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f"},
|
{file = "mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f"},
|
||||||
@ -764,6 +793,7 @@ version = "0.2.0"
|
|||||||
description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file"
|
description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"},
|
{file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"},
|
||||||
{file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
|
{file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
|
||||||
@ -780,6 +810,7 @@ version = "9.5.49"
|
|||||||
description = "Documentation that simply works"
|
description = "Documentation that simply works"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mkdocs_material-9.5.49-py3-none-any.whl", hash = "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e"},
|
{file = "mkdocs_material-9.5.49-py3-none-any.whl", hash = "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e"},
|
||||||
{file = "mkdocs_material-9.5.49.tar.gz", hash = "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d"},
|
{file = "mkdocs_material-9.5.49.tar.gz", hash = "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d"},
|
||||||
@ -809,6 +840,7 @@ version = "1.3.1"
|
|||||||
description = "Extension pack for Python Markdown and MkDocs Material."
|
description = "Extension pack for Python Markdown and MkDocs Material."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
|
{file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
|
||||||
{file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
|
{file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
|
||||||
@ -820,6 +852,7 @@ version = "0.27.0"
|
|||||||
description = "Automatic documentation from sources, for MkDocs."
|
description = "Automatic documentation from sources, for MkDocs."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332"},
|
{file = "mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332"},
|
||||||
{file = "mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657"},
|
{file = "mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657"},
|
||||||
@ -846,6 +879,7 @@ version = "1.12.2"
|
|||||||
description = "A Python handler for mkdocstrings."
|
description = "A Python handler for mkdocstrings."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a"},
|
{file = "mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a"},
|
||||||
{file = "mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3"},
|
{file = "mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3"},
|
||||||
@ -862,6 +896,7 @@ version = "0.2.3"
|
|||||||
description = "multi volume file wrapper library"
|
description = "multi volume file wrapper library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "multivolumefile-0.2.3-py3-none-any.whl", hash = "sha256:237f4353b60af1703087cf7725755a1f6fcaeeea48421e1896940cd1c920d678"},
|
{file = "multivolumefile-0.2.3-py3-none-any.whl", hash = "sha256:237f4353b60af1703087cf7725755a1f6fcaeeea48421e1896940cd1c920d678"},
|
||||||
{file = "multivolumefile-0.2.3.tar.gz", hash = "sha256:a0648d0aafbc96e59198d5c17e9acad7eb531abea51035d08ce8060dcad709d6"},
|
{file = "multivolumefile-0.2.3.tar.gz", hash = "sha256:a0648d0aafbc96e59198d5c17e9acad7eb531abea51035d08ce8060dcad709d6"},
|
||||||
@ -878,6 +913,7 @@ version = "1.8.0"
|
|||||||
description = "Node.js virtual environment builder"
|
description = "Node.js virtual environment builder"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
|
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
|
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
|
||||||
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
|
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
|
||||||
@ -892,6 +928,7 @@ version = "23.2"
|
|||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main", "dev", "docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
||||||
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
||||||
@ -903,6 +940,7 @@ version = "0.5.7"
|
|||||||
description = "Divides large result sets into pages for easier browsing"
|
description = "Divides large result sets into pages for easier browsing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"},
|
{file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"},
|
||||||
{file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"},
|
{file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"},
|
||||||
@ -918,6 +956,7 @@ version = "0.12.1"
|
|||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||||
@ -929,6 +968,7 @@ version = "3.11.0"
|
|||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main", "dev", "docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
|
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
|
||||||
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
|
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
|
||||||
@ -944,6 +984,7 @@ version = "1.5.0"
|
|||||||
description = "plugin and hook calling mechanisms for python"
|
description = "plugin and hook calling mechanisms for python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||||
@ -959,6 +1000,7 @@ version = "3.7.0"
|
|||||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"},
|
{file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"},
|
||||||
{file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"},
|
{file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"},
|
||||||
@ -977,6 +1019,8 @@ version = "6.1.0"
|
|||||||
description = "Cross-platform lib for process and system monitoring in Python."
|
description = "Cross-platform lib for process and system monitoring in Python."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "sys_platform != \"cygwin\""
|
||||||
files = [
|
files = [
|
||||||
{file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"},
|
{file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"},
|
||||||
{file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"},
|
{file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"},
|
||||||
@ -1007,6 +1051,7 @@ version = "0.22.0"
|
|||||||
description = "Pure python 7-zip library"
|
description = "Pure python 7-zip library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "py7zr-0.22.0-py3-none-any.whl", hash = "sha256:993b951b313500697d71113da2681386589b7b74f12e48ba13cc12beca79d078"},
|
{file = "py7zr-0.22.0-py3-none-any.whl", hash = "sha256:993b951b313500697d71113da2681386589b7b74f12e48ba13cc12beca79d078"},
|
||||||
{file = "py7zr-0.22.0.tar.gz", hash = "sha256:c6c7aea5913535184003b73938490f9a4d8418598e533f9ca991d3b8e45a139e"},
|
{file = "py7zr-0.22.0.tar.gz", hash = "sha256:c6c7aea5913535184003b73938490f9a4d8418598e533f9ca991d3b8e45a139e"},
|
||||||
@ -1037,6 +1082,7 @@ version = "1.0.2"
|
|||||||
description = "bcj filter library"
|
description = "bcj filter library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pybcj-1.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7bff28d97e47047d69a4ac6bf59adda738cf1d00adde8819117fdb65d966bdbc"},
|
{file = "pybcj-1.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7bff28d97e47047d69a4ac6bf59adda738cf1d00adde8819117fdb65d966bdbc"},
|
||||||
{file = "pybcj-1.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:198e0b4768b4025eb3309273d7e81dc53834b9a50092be6e0d9b3983cfd35c35"},
|
{file = "pybcj-1.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:198e0b4768b4025eb3309273d7e81dc53834b9a50092be6e0d9b3983cfd35c35"},
|
||||||
@ -1091,6 +1137,8 @@ version = "2.22"
|
|||||||
description = "C parser in Python"
|
description = "C parser in Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "platform_python_implementation == \"PyPy\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||||
@ -1102,6 +1150,7 @@ version = "3.21.0"
|
|||||||
description = "Cryptographic library for Python"
|
description = "Cryptographic library for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"},
|
{file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"},
|
||||||
{file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"},
|
{file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"},
|
||||||
@ -1143,6 +1192,7 @@ version = "2.18.0"
|
|||||||
description = "Pygments is a syntax highlighting package written in Python."
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
||||||
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
||||||
@ -1157,6 +1207,7 @@ version = "10.12"
|
|||||||
description = "Extension pack for Python Markdown."
|
description = "Extension pack for Python Markdown."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"},
|
{file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"},
|
||||||
{file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"},
|
{file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"},
|
||||||
@ -1175,6 +1226,7 @@ version = "1.1.0"
|
|||||||
description = "PPMd compression/decompression library"
|
description = "PPMd compression/decompression library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pyppmd-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5cd428715413fe55abf79dc9fc54924ba7e518053e1fc0cbdf80d0d99cf1442"},
|
{file = "pyppmd-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5cd428715413fe55abf79dc9fc54924ba7e518053e1fc0cbdf80d0d99cf1442"},
|
||||||
{file = "pyppmd-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e96cc43f44b7658be2ea764e7fa99c94cb89164dbb7cdf209178effc2168319"},
|
{file = "pyppmd-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e96cc43f44b7658be2ea764e7fa99c94cb89164dbb7cdf209178effc2168319"},
|
||||||
@ -1261,6 +1313,7 @@ version = "7.4.4"
|
|||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
|
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
|
||||||
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
|
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
|
||||||
@ -1281,6 +1334,7 @@ version = "2.9.0.post0"
|
|||||||
description = "Extensions to the standard Python datetime module"
|
description = "Extensions to the standard Python datetime module"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||||
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||||
@ -1295,6 +1349,7 @@ version = "6.0.1"
|
|||||||
description = "YAML parser and emitter for Python"
|
description = "YAML parser and emitter for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
groups = ["dev", "docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
|
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
|
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
|
||||||
@ -1355,6 +1410,7 @@ version = "0.1"
|
|||||||
description = "A custom YAML tag for referencing environment variables in YAML files. "
|
description = "A custom YAML tag for referencing environment variables in YAML files. "
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
|
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
|
||||||
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
|
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
|
||||||
@ -1369,6 +1425,7 @@ version = "0.16.2"
|
|||||||
description = "Python bindings to Zstandard (zstd) compression library."
|
description = "Python bindings to Zstandard (zstd) compression library."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pyzstd-0.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:637376c8f8cbd0afe1cab613f8c75fd502bd1016bf79d10760a2d5a00905fe62"},
|
{file = "pyzstd-0.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:637376c8f8cbd0afe1cab613f8c75fd502bd1016bf79d10760a2d5a00905fe62"},
|
||||||
{file = "pyzstd-0.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e7a7118cbcfa90ca2ddbf9890c7cb582052a9a8cf2b7e2c1bbaf544bee0f16a"},
|
{file = "pyzstd-0.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e7a7118cbcfa90ca2ddbf9890c7cb582052a9a8cf2b7e2c1bbaf544bee0f16a"},
|
||||||
@ -1461,6 +1518,7 @@ version = "3.9.0"
|
|||||||
description = "rapid fuzzy string matching"
|
description = "rapid fuzzy string matching"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bd375c4830fee11d502dd93ecadef63c137ae88e1aaa29cc15031fa66d1e0abb"},
|
{file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bd375c4830fee11d502dd93ecadef63c137ae88e1aaa29cc15031fa66d1e0abb"},
|
||||||
{file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:55e2c5076f38fc1dbaacb95fa026a3e409eee6ea5ac4016d44fb30e4cad42b20"},
|
{file = "rapidfuzz-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:55e2c5076f38fc1dbaacb95fa026a3e409eee6ea5ac4016d44fb30e4cad42b20"},
|
||||||
@ -1563,6 +1621,7 @@ version = "2024.11.6"
|
|||||||
description = "Alternative regular expression module, to replace re."
|
description = "Alternative regular expression module, to replace re."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
|
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
|
||||||
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
|
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
|
||||||
@ -1666,6 +1725,7 @@ version = "2.31.0"
|
|||||||
description = "Python HTTP for Humans."
|
description = "Python HTTP for Humans."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main", "docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
|
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
|
||||||
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
||||||
@ -1687,6 +1747,7 @@ version = "69.5.1"
|
|||||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
|
{file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
|
||||||
{file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
|
{file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
|
||||||
@ -1703,6 +1764,7 @@ version = "1.17.0"
|
|||||||
description = "Python 2 and 3 compatibility utilities"
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||||
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||||
@ -1714,6 +1776,7 @@ version = "1.7.0"
|
|||||||
description = "module to create simple ASCII tables"
|
description = "module to create simple ASCII tables"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917"},
|
{file = "texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917"},
|
||||||
{file = "texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638"},
|
{file = "texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638"},
|
||||||
@ -1725,6 +1788,7 @@ version = "4.66.4"
|
|||||||
description = "Fast, Extensible Progress Meter"
|
description = "Fast, Extensible Progress Meter"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["cli"]
|
||||||
files = [
|
files = [
|
||||||
{file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"},
|
{file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"},
|
||||||
{file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"},
|
{file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"},
|
||||||
@ -1745,6 +1809,7 @@ version = "2.2.1"
|
|||||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main", "docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
|
{file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
|
||||||
{file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
|
{file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
|
||||||
@ -1762,6 +1827,7 @@ version = "20.26.1"
|
|||||||
description = "Virtual Python Environment builder"
|
description = "Virtual Python Environment builder"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"},
|
{file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"},
|
||||||
{file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"},
|
{file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"},
|
||||||
@ -1782,6 +1848,7 @@ version = "6.0.0"
|
|||||||
description = "Filesystem events monitoring"
|
description = "Filesystem events monitoring"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
|
groups = ["docs"]
|
||||||
files = [
|
files = [
|
||||||
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
|
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
|
||||||
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
|
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
|
||||||
@ -1819,6 +1886,6 @@ files = [
|
|||||||
watchmedo = ["PyYAML (>=3.10)"]
|
watchmedo = ["PyYAML (>=3.10)"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "90e32b6cbb82fb04e538c715261f41ea80aeb29cbd829eace009afcf8142605e"
|
content-hash = "f3588d29d3944238b6dcba732acbc5e4591d54a9c6136555a43a5523a8121b3e"
|
||||||
|
|||||||
@ -13,6 +13,7 @@ requests = "^2.31.0"
|
|||||||
cleo = "^2.1.0"
|
cleo = "^2.1.0"
|
||||||
packaging = "^23.2"
|
packaging = "^23.2"
|
||||||
py7zr = "^0.22.0"
|
py7zr = "^0.22.0"
|
||||||
|
multivolumefile = "^0.2.3"
|
||||||
|
|
||||||
[tool.poetry.group.cli]
|
[tool.poetry.group.cli]
|
||||||
optional = true
|
optional = true
|
||||||
|
|||||||
@ -114,7 +114,21 @@ class GameABC(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_update(self):
|
def get_version_config(self) -> tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Gets the current installed game version from config.ini.
|
||||||
|
|
||||||
|
Using this is not recommended, as only official launcher creates
|
||||||
|
and uses this file, instead you should use `get_version()`.
|
||||||
|
|
||||||
|
This returns (0, 0, 0) if the version could not be found.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[int, int, int]: Game version.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_update(self) -> resource.Patch | None:
|
||||||
"""
|
"""
|
||||||
Get the game update
|
Get the game update
|
||||||
"""
|
"""
|
||||||
@ -126,7 +140,9 @@ class GameABC(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_remote_game(self, pre_download: bool = False) -> resource.Main | resource.PreDownload:
|
def get_remote_game(
|
||||||
|
self, pre_download: bool = False
|
||||||
|
) -> resource.Main | resource.PreDownload:
|
||||||
"""
|
"""
|
||||||
Gets the current game information from remote.
|
Gets the current game information from remote.
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from cleo.helpers import option, argument
|
|||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from platform import system
|
from platform import system
|
||||||
from vollerei.abc.launcher.game import GameABC
|
from vollerei.abc.launcher.game import GameABC
|
||||||
|
from vollerei.common.api import resource
|
||||||
from vollerei.common.enums import GameChannel, VoicePackLanguage
|
from vollerei.common.enums import GameChannel, VoicePackLanguage
|
||||||
from vollerei.cli import utils
|
from vollerei.cli import utils
|
||||||
from vollerei.exceptions.game import GameError
|
from vollerei.exceptions.game import GameError
|
||||||
@ -141,9 +142,7 @@ class VoicepackList(Command):
|
|||||||
|
|
||||||
class VoicepackInstall(Command):
|
class VoicepackInstall(Command):
|
||||||
name = "hsr voicepack install"
|
name = "hsr voicepack install"
|
||||||
description = (
|
description = "Installs the specified installed voicepacks"
|
||||||
"Installs the specified installed voicepacks"
|
|
||||||
)
|
|
||||||
options = default_options + [
|
options = default_options + [
|
||||||
option("pre-download", description="Pre-download the game if available"),
|
option("pre-download", description="Pre-download the game if available"),
|
||||||
]
|
]
|
||||||
@ -196,7 +195,9 @@ class VoicepackInstall(Command):
|
|||||||
self.line(
|
self.line(
|
||||||
f"Downloading install package for language: <comment>{remote_voicepack.language.name}</comment>... "
|
f"Downloading install package for language: <comment>{remote_voicepack.language.name}</comment>... "
|
||||||
)
|
)
|
||||||
archive_file = State.game.cache.joinpath(PurePath(remote_voicepack.url).name)
|
archive_file = State.game.cache.joinpath(
|
||||||
|
PurePath(remote_voicepack.url).name
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
download_result = utils.download(
|
download_result = utils.download(
|
||||||
remote_voicepack.url, archive_file, file_len=remote_voicepack.size
|
remote_voicepack.url, archive_file, file_len=remote_voicepack.size
|
||||||
@ -280,7 +281,9 @@ class VoicepackUpdate(Command):
|
|||||||
progress = utils.ProgressIndicator(self)
|
progress = utils.ProgressIndicator(self)
|
||||||
progress.start("Checking for updates... ")
|
progress.start("Checking for updates... ")
|
||||||
try:
|
try:
|
||||||
update_diff = State.game.get_update(pre_download=pre_download)
|
update_diff: resource.Patch | None = State.game.get_update(
|
||||||
|
pre_download=pre_download
|
||||||
|
)
|
||||||
game_info = State.game.get_remote_game(pre_download=pre_download)
|
game_info = State.game.get_remote_game(pre_download=pre_download)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
progress.finish(
|
progress.finish(
|
||||||
@ -295,23 +298,25 @@ class VoicepackUpdate(Command):
|
|||||||
f"The current version is: <comment>{State.game.get_version_str()}</comment>"
|
f"The current version is: <comment>{State.game.get_version_str()}</comment>"
|
||||||
)
|
)
|
||||||
self.line(
|
self.line(
|
||||||
f"The latest version is: <comment>{game_info.latest.version}</comment>"
|
f"The latest version is: <comment>{game_info.major.version}</comment>"
|
||||||
)
|
)
|
||||||
if not self.confirm("Do you want to update the game?"):
|
if not self.confirm("Do you want to update the game?"):
|
||||||
self.line("<error>Update aborted.</error>")
|
self.line("<error>Update aborted.</error>")
|
||||||
return
|
return
|
||||||
# Voicepack update
|
# Voicepack update
|
||||||
for remote_voicepack in update_diff.voice_packs:
|
for remote_voicepack in update_diff.audio_pkgs:
|
||||||
if remote_voicepack.language not in installed_voicepacks:
|
if remote_voicepack.language not in installed_voicepacks:
|
||||||
continue
|
continue
|
||||||
# Voicepack is installed, update it
|
# Voicepack is installed, update it
|
||||||
self.line(
|
archive_file = State.game.cache.joinpath(
|
||||||
f"Downloading update package for language: <comment>{remote_voicepack.language.name}</comment>... "
|
PurePath(remote_voicepack.url).name
|
||||||
|
)
|
||||||
|
self.line(
|
||||||
|
f"Downloading update package for voicepack language '{remote_voicepack.language.name}'..."
|
||||||
)
|
)
|
||||||
archive_file = State.game.cache.joinpath(remote_voicepack.name)
|
|
||||||
try:
|
try:
|
||||||
download_result = utils.download(
|
download_result = utils.download(
|
||||||
remote_voicepack.path, archive_file, file_len=update_diff.size
|
remote_voicepack.url, archive_file, file_len=remote_voicepack.size
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.line_error(f"<error>Couldn't download update: {e}</error>")
|
self.line_error(f"<error>Couldn't download update: {e}</error>")
|
||||||
@ -334,10 +339,9 @@ class VoicepackUpdate(Command):
|
|||||||
progress.finish(
|
progress.finish(
|
||||||
f"<comment>Update applied for language {remote_voicepack.language.name}.</comment>"
|
f"<comment>Update applied for language {remote_voicepack.language.name}.</comment>"
|
||||||
)
|
)
|
||||||
|
State.game.version_override = game_info.major.version
|
||||||
set_version_config(self=self)
|
set_version_config(self=self)
|
||||||
self.line(
|
State.game.version_override = None
|
||||||
f"The game has been updated to version: <comment>{State.game.get_version_str()}</comment>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PatchTypeCommand(Command):
|
class PatchTypeCommand(Command):
|
||||||
@ -601,7 +605,7 @@ class InstallCommand(Command):
|
|||||||
progress.finish("<comment>Package applied for the base game.</comment>")
|
progress.finish("<comment>Package applied for the base game.</comment>")
|
||||||
self.line("Setting version config... ")
|
self.line("Setting version config... ")
|
||||||
State.game.version_override = game_info.major.version
|
State.game.version_override = game_info.major.version
|
||||||
set_version_config()
|
set_version_config(self=self)
|
||||||
State.game.version_override = None
|
State.game.version_override = None
|
||||||
self.line(
|
self.line(
|
||||||
f"The game has been installed to version: <comment>{State.game.get_version_str()}</comment>"
|
f"The game has been installed to version: <comment>{State.game.get_version_str()}</comment>"
|
||||||
@ -719,7 +723,7 @@ class UpdateCommand(Command):
|
|||||||
)
|
)
|
||||||
self.line("Setting version config... ")
|
self.line("Setting version config... ")
|
||||||
State.game.version_override = game_info.major.version
|
State.game.version_override = game_info.major.version
|
||||||
set_version_config()
|
set_version_config(self=self)
|
||||||
State.game.version_override = None
|
State.game.version_override = None
|
||||||
self.line(
|
self.line(
|
||||||
f"The game has been updated to version: <comment>{State.game.get_version_str()}</comment>"
|
f"The game has been updated to version: <comment>{State.game.get_version_str()}</comment>"
|
||||||
@ -940,7 +944,8 @@ class ApplyUpdateArchive(Command):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
progress.finish("<comment>Update applied.</comment>")
|
progress.finish("<comment>Update applied.</comment>")
|
||||||
set_version_config()
|
set_version_config(self=self)
|
||||||
|
|
||||||
|
|
||||||
# This is the list for HSR commands, we'll add Genshin commands later
|
# This is the list for HSR commands, we'll add Genshin commands later
|
||||||
classes = [
|
classes = [
|
||||||
@ -953,7 +958,7 @@ classes = [
|
|||||||
PatchInstallCommand,
|
PatchInstallCommand,
|
||||||
PatchTelemetryCommand,
|
PatchTelemetryCommand,
|
||||||
PatchTypeCommand,
|
PatchTypeCommand,
|
||||||
RepairCommand,
|
# RepairCommand,
|
||||||
UpdatePatchCommand,
|
UpdatePatchCommand,
|
||||||
UpdateCommand,
|
UpdateCommand,
|
||||||
UpdateDownloadCommand,
|
UpdateDownloadCommand,
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class GameType(Enum):
|
||||||
|
Genshin = 0
|
||||||
|
HSR = 1
|
||||||
|
ZZZ = 3
|
||||||
|
HI3 = 4
|
||||||
|
|
||||||
|
|
||||||
class GameChannel(Enum):
|
class GameChannel(Enum):
|
||||||
Overseas = 0
|
Overseas = 0
|
||||||
China = 1
|
China = 1
|
||||||
@ -49,4 +56,4 @@ class VoicePackLanguage(Enum):
|
|||||||
elif s == "En":
|
elif s == "En":
|
||||||
return VoicePackLanguage.English
|
return VoicePackLanguage.English
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid language string: {s}")
|
raise ValueError(f"Invalid language string: {s}")
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import multivolumefile
|
||||||
import py7zr
|
import py7zr
|
||||||
|
import zipfile
|
||||||
from io import IOBase
|
from io import IOBase
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -10,7 +12,6 @@ from vollerei.abc.launcher.game import GameABC
|
|||||||
from vollerei.common.api import resource
|
from vollerei.common.api import resource
|
||||||
from vollerei.exceptions.game import (
|
from vollerei.exceptions.game import (
|
||||||
RepairError,
|
RepairError,
|
||||||
GameAlreadyInstalledError,
|
|
||||||
GameNotInstalledError,
|
GameNotInstalledError,
|
||||||
ScatteredFilesNotAvailableError,
|
ScatteredFilesNotAvailableError,
|
||||||
)
|
)
|
||||||
@ -20,6 +21,30 @@ from vollerei.utils import HDiffPatch, HPatchZPatchError, download
|
|||||||
_hdiff = HDiffPatch()
|
_hdiff = HDiffPatch()
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_files(
|
||||||
|
archive: py7zr.SevenZipFile | zipfile.ZipFile, files, path: PathLike
|
||||||
|
):
|
||||||
|
if isinstance(archive, py7zr.SevenZipFile):
|
||||||
|
# .7z archive
|
||||||
|
archive.extract(path, files)
|
||||||
|
else:
|
||||||
|
# .zip archive
|
||||||
|
archive.extractall(path, files)
|
||||||
|
|
||||||
|
|
||||||
|
def _open_archive(file: Path | IOBase) -> py7zr.SevenZipFile | zipfile.ZipFile:
|
||||||
|
archive: py7zr.SevenZipFile | zipfile.ZipFile = None
|
||||||
|
try:
|
||||||
|
archive = py7zr.SevenZipFile(file, "r")
|
||||||
|
except py7zr.exceptions.Bad7zFile:
|
||||||
|
# Try to open it as a zip file
|
||||||
|
try:
|
||||||
|
archive = zipfile.ZipFile(file, "r")
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
raise ValueError("Archive is not a valid 7z or zip file.")
|
||||||
|
return archive
|
||||||
|
|
||||||
|
|
||||||
def apply_update_archive(
|
def apply_update_archive(
|
||||||
game: GameABC, archive_file: Path | IOBase, auto_repair: bool = True
|
game: GameABC, archive_file: Path | IOBase, auto_repair: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -39,31 +64,41 @@ def apply_update_archive(
|
|||||||
|
|
||||||
# Install HDiffPatch
|
# Install HDiffPatch
|
||||||
_hdiff.hpatchz()
|
_hdiff.hpatchz()
|
||||||
|
|
||||||
# Open archive
|
# Open archive
|
||||||
# archive = zipfile.ZipFile(archive_file, "r")
|
def reset_if_py7zr(archive):
|
||||||
archive = py7zr.SevenZipFile(archive_file, "r")
|
if isinstance(archive, py7zr.SevenZipFile):
|
||||||
|
archive.reset()
|
||||||
|
|
||||||
|
archive = _open_archive(archive_file)
|
||||||
|
|
||||||
# Get files list (we don't want to extract all of them)
|
# Get files list (we don't want to extract all of them)
|
||||||
files = archive.namelist()
|
files = archive.namelist()
|
||||||
# Don't extract these files (they're useless and if the game isn't patched then
|
# Don't extract these files (they're useless and if the game isn't patched then
|
||||||
# it'll raise 31-4xxx error in Genshin)
|
# it'll raise 31-4xxx error in Genshin)
|
||||||
for file in ["deletefiles.txt", "hdifffiles.txt"]:
|
for file in ["deletefiles.txt", "hdifffiles.txt", "hdiffmap.json"]:
|
||||||
try:
|
try:
|
||||||
files.remove(file)
|
files.remove(file)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
# Think for me a better name for this variable
|
# Think for me a better name for this variable
|
||||||
txtfiles = archive.read(["deletefiles.txt", "hdifffiles.txt"])
|
txtfiles = None
|
||||||
# Reset archive to extract files
|
if isinstance(archive, py7zr.SevenZipFile):
|
||||||
archive.reset()
|
txtfiles = archive.read(["deletefiles.txt", "hdifffiles.txt", "hdiffmap.json"])
|
||||||
|
# Reset archive to extract files
|
||||||
|
archive.reset()
|
||||||
try:
|
try:
|
||||||
# miHoYo loves CRLF
|
# miHoYo loves CRLF
|
||||||
deletebytes = txtfiles["deletefiles.txt"].read()
|
if txtfiles is not None:
|
||||||
|
deletebytes = txtfiles["deletefiles.txt"].read()
|
||||||
|
else:
|
||||||
|
deletebytes = archive.read("deletefiles.txt")
|
||||||
if deletebytes is not str:
|
if deletebytes is not str:
|
||||||
# Typing
|
# Typing
|
||||||
deletebytes: bytes
|
deletebytes: bytes
|
||||||
deletebytes = deletebytes.decode()
|
deletebytes = deletebytes.decode()
|
||||||
deletefiles = deletebytes.split("\r\n")
|
deletefiles = deletebytes.split("\r\n")
|
||||||
except IOError:
|
except (IOError, KeyError):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
for file_str in deletefiles:
|
for file_str in deletefiles:
|
||||||
@ -79,61 +114,89 @@ def apply_update_archive(
|
|||||||
|
|
||||||
# hdiffpatch implementation
|
# hdiffpatch implementation
|
||||||
# Read hdifffiles.txt to get the files to patch
|
# Read hdifffiles.txt to get the files to patch
|
||||||
hdifffiles = []
|
# Hdifffile format is [(source file, target file)]
|
||||||
hdiffbytes = txtfiles["hdifffiles.txt"].read()
|
# While the patch file is named as target file + ".hdiff"
|
||||||
|
hdifffiles: list[tuple[str, str]] = []
|
||||||
|
new_hdiff_map = False
|
||||||
|
if txtfiles is not None:
|
||||||
|
old_hdiff_map = txtfiles.get("hdifffiles.txt")
|
||||||
|
if old_hdiff_map is not None:
|
||||||
|
hdiffbytes = old_hdiff_map.read()
|
||||||
|
else:
|
||||||
|
new_hdiff_map = True
|
||||||
|
hdiffbytes = txtfiles["hdiffmap.json"].read()
|
||||||
|
else:
|
||||||
|
# Archive file must be a zip file
|
||||||
|
if zipfile.Path(archive).joinpath("hdifffiles.txt").is_file():
|
||||||
|
hdiffbytes = archive.read("hdifffiles.txt")
|
||||||
|
else:
|
||||||
|
new_hdiff_map = True
|
||||||
|
hdiffbytes = archive.read("hdiffmap.json")
|
||||||
if hdiffbytes is not str:
|
if hdiffbytes is not str:
|
||||||
# Typing
|
# Typing
|
||||||
hdiffbytes: bytes
|
hdiffbytes: bytes
|
||||||
hdiffbytes = hdiffbytes.decode()
|
hdiffbytes = hdiffbytes.decode()
|
||||||
for x in hdiffbytes.split("\r\n"):
|
if new_hdiff_map:
|
||||||
try:
|
mapping = json.loads(hdiffbytes)
|
||||||
hdifffiles.append(json.loads(x.strip())["remoteName"])
|
for diff in mapping["diff_map"]:
|
||||||
except json.JSONDecodeError:
|
hdifffiles.append((diff["source_file_name"], diff["target_file_name"]))
|
||||||
pass
|
else:
|
||||||
|
for x in hdiffbytes.split("\r\n"):
|
||||||
|
try:
|
||||||
|
name = json.loads(x.strip())["remoteName"]
|
||||||
|
hdifffiles.append((name, name))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Patch function
|
# Patch function
|
||||||
def patch(file, patch_file):
|
def patch(source_file: Path, target_file: Path, patch_file: str):
|
||||||
patchpath = game.cache.joinpath(patch_file)
|
patch_path = game.cache.joinpath(patch_file)
|
||||||
# Spaghetti code :(, fuck my eyes.
|
# Spaghetti code :(, fuck my eyes.
|
||||||
file = file.rename(file.with_suffix(file.suffix + ".bak"))
|
bak_src_file = source_file.rename(
|
||||||
|
source_file.with_suffix(source_file.suffix + ".bak")
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
_hdiff.patch_file(file, file.with_suffix(""), patchpath)
|
_hdiff.patch_file(bak_src_file, target_file, patch_path)
|
||||||
except HPatchZPatchError:
|
except HPatchZPatchError:
|
||||||
if auto_repair:
|
if auto_repair:
|
||||||
try:
|
try:
|
||||||
game.repair_file(game.path.joinpath(file.with_suffix("")))
|
# The game repairs file by downloading the latest file, in this case we want the target file
|
||||||
|
# instead of source file. Honestly I haven't tested this but I hope it works.
|
||||||
|
game.repair_file(target_file)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Let the game download the file.
|
# Let the game download the file.
|
||||||
file.rename(file.with_suffix(""))
|
bak_src_file.rename(file.with_suffix(""))
|
||||||
else:
|
else:
|
||||||
file.unlink()
|
bak_src_file.unlink()
|
||||||
else:
|
else:
|
||||||
# Let the game download the file.
|
# Let the game download the file.
|
||||||
file.rename(file.with_suffix(""))
|
bak_src_file.rename(file.with_suffix(""))
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
# Remove old file, since we don't need it anymore.
|
||||||
|
bak_src_file.unlink()
|
||||||
finally:
|
finally:
|
||||||
patchpath.unlink()
|
patch_path.unlink()
|
||||||
# Remove old file, since we don't need it anymore.
|
|
||||||
file.unlink()
|
|
||||||
|
|
||||||
# Multi-threaded patching
|
# Multi-threaded patching
|
||||||
patch_jobs = []
|
patch_jobs = []
|
||||||
patch_files = []
|
patch_files = []
|
||||||
for file_str in hdifffiles:
|
for source_file, target_file in hdifffiles:
|
||||||
file = game.path.joinpath(file_str)
|
source_path = game.path.joinpath(source_file)
|
||||||
if not file.exists():
|
if not source_path.exists():
|
||||||
# Not patching since we don't have the file
|
# Not patching since we don't have the file
|
||||||
continue
|
continue
|
||||||
patch_file: str = file_str + ".hdiff"
|
target_path = game.path.joinpath(target_file)
|
||||||
|
patch_file: str = target_file + ".hdiff"
|
||||||
# Remove hdiff files from files list to extract
|
# Remove hdiff files from files list to extract
|
||||||
files.remove(patch_file)
|
files.remove(patch_file)
|
||||||
# Add file to extract list
|
# Add file to extract list
|
||||||
patch_files.append(patch_file)
|
patch_files.append(patch_file)
|
||||||
patch_jobs.append([patch, [file, patch_file]])
|
patch_jobs.append([patch, [source_path, target_path, patch_file]])
|
||||||
|
|
||||||
# Extract patch files to temporary dir
|
# Extract patch files to temporary dir
|
||||||
archive.extract(game.cache, patch_files)
|
_extract_files(archive, patch_files, game.cache)
|
||||||
archive.reset() # For the next extraction
|
reset_if_py7zr(archive) # For the next extraction
|
||||||
# Create new ThreadPoolExecutor for patching
|
# Create new ThreadPoolExecutor for patching
|
||||||
patch_executor = concurrent.futures.ThreadPoolExecutor()
|
patch_executor = concurrent.futures.ThreadPoolExecutor()
|
||||||
for job in patch_jobs:
|
for job in patch_jobs:
|
||||||
@ -141,7 +204,7 @@ def apply_update_archive(
|
|||||||
patch_executor.shutdown(wait=True)
|
patch_executor.shutdown(wait=True)
|
||||||
|
|
||||||
# Extract files from archive after we have filtered out the patch files
|
# Extract files from archive after we have filtered out the patch files
|
||||||
archive.extract(game.path, files)
|
_extract_files(archive, files, game.path)
|
||||||
|
|
||||||
# Close the archive
|
# Close the archive
|
||||||
archive.close()
|
archive.close()
|
||||||
@ -152,16 +215,32 @@ def install_archive(game: GameABC, archive_file: Path | IOBase) -> None:
|
|||||||
Applies an install archive to the game, it can be the game itself or a
|
Applies an install archive to the game, it can be the game itself or a
|
||||||
voicepack one.
|
voicepack one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
game (GameABC): The game to install the archive for.
|
||||||
|
archive_file (Path | IOBase): The archive file to install, if it's a
|
||||||
|
split archive then this is the first part.
|
||||||
|
|
||||||
Because this function is shared for all games, you should use the game's
|
Because this function is shared for all games, you should use the game's
|
||||||
`install_archive()` method instead, which additionally applies required
|
`install_archive()` method instead, which additionally applies required
|
||||||
methods for that game.
|
methods for that game.
|
||||||
"""
|
"""
|
||||||
if game.is_installed():
|
archive: py7zr.SevenZipFile | zipfile.ZipFile = None
|
||||||
raise GameAlreadyInstalledError("Game is already installed.")
|
archive_path = Path(archive_file)
|
||||||
# It's literally 3 lines but okay
|
target_archive = None
|
||||||
archive = py7zr.SevenZipFile(archive_file, "r")
|
if archive_path.suffix == ".001":
|
||||||
|
archive_path_merged = archive_path.with_suffix("")
|
||||||
|
target_archive = multivolumefile.open(archive_path_merged, mode='rb')
|
||||||
|
if archive_path_merged.suffix == ".zip":
|
||||||
|
# .zip split archive
|
||||||
|
archive = zipfile.ZipFile(target_archive, "r")
|
||||||
|
else:
|
||||||
|
archive = py7zr.SevenZipFile(target_archive, "r")
|
||||||
|
else:
|
||||||
|
archive = _open_archive(archive_file)
|
||||||
archive.extractall(game.path)
|
archive.extractall(game.path)
|
||||||
archive.close()
|
archive.close()
|
||||||
|
if target_archive:
|
||||||
|
target_archive.close()
|
||||||
|
|
||||||
|
|
||||||
def _repair_file(game: GameABC, file: PathLike, game_info: resource.Main) -> None:
|
def _repair_file(game: GameABC, file: PathLike, game_info: resource.Main) -> None:
|
||||||
|
|||||||
0
vollerei/game/__init__.py
Normal file
0
vollerei/game/__init__.py
Normal file
86
vollerei/game/genshin/functions.py
Normal file
86
vollerei/game/genshin/functions.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from vollerei.common.enums import GameChannel
|
||||||
|
from vollerei.abc.launcher.game import GameABC
|
||||||
|
from vollerei.exceptions.game import GameNotInstalledError
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel(game: GameABC) -> GameChannel:
|
||||||
|
"""
|
||||||
|
Gets the current game channel.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GameChannel: The current game channel.
|
||||||
|
"""
|
||||||
|
if game.channel_override:
|
||||||
|
return game.channel_override
|
||||||
|
if not game.is_installed():
|
||||||
|
raise GameNotInstalledError("Game path is not set.")
|
||||||
|
if game.path.joinpath("YuanShen.exe").is_file():
|
||||||
|
return GameChannel.China
|
||||||
|
return GameChannel.Overseas
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(game: GameABC) -> tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Gets the current installed game version.
|
||||||
|
|
||||||
|
Credits to An Anime Team for the code that does the magic:
|
||||||
|
https://github.com/an-anime-team/anime-game-core/blob/main/src/games/genshin/game.rs#L52
|
||||||
|
|
||||||
|
If the above method fails, it'll fallback to read the config.ini file
|
||||||
|
for the version, which is not recommended (as described in
|
||||||
|
`get_version_config()` docs)
|
||||||
|
|
||||||
|
This returns (0, 0, 0) if the version could not be found
|
||||||
|
(usually indicates the game is not installed), and in fact `is_installed()` uses
|
||||||
|
this method to check if the game is installed too.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[int, int, int]: The version as a tuple of integers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_file = game.data_folder().joinpath("globalgamemanagers")
|
||||||
|
if not data_file.exists():
|
||||||
|
return game.get_version_config()
|
||||||
|
|
||||||
|
def bytes_to_int(byte_array: list[bytes]) -> int:
|
||||||
|
bytes_as_int = int.from_bytes(byte_array, byteorder="big")
|
||||||
|
actual_int = bytes_as_int - 48 # 48 is the ASCII code for 0
|
||||||
|
return actual_int
|
||||||
|
|
||||||
|
version_bytes: list[list[bytes]] = [[], [], []]
|
||||||
|
version_ptr = 0
|
||||||
|
correct = True
|
||||||
|
try:
|
||||||
|
with data_file.open("rb") as f:
|
||||||
|
f.seek(4000)
|
||||||
|
for byte in f.read(10000):
|
||||||
|
match byte:
|
||||||
|
case 0:
|
||||||
|
version_bytes = [[], [], []]
|
||||||
|
version_ptr = 0
|
||||||
|
correct = True
|
||||||
|
case 46:
|
||||||
|
version_ptr += 1
|
||||||
|
if version_ptr > 2:
|
||||||
|
correct = False
|
||||||
|
case 95:
|
||||||
|
if (
|
||||||
|
correct
|
||||||
|
and len(version_bytes[0]) > 0
|
||||||
|
and len(version_bytes[1]) > 0
|
||||||
|
and len(version_bytes[2]) > 0
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
bytes_to_int(version_bytes[0]),
|
||||||
|
bytes_to_int(version_bytes[1]),
|
||||||
|
bytes_to_int(version_bytes[2]),
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
if correct and byte in b"0123456789":
|
||||||
|
version_bytes[version_ptr].append(byte)
|
||||||
|
else:
|
||||||
|
correct = False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Fallback to config.ini
|
||||||
|
return game.get_version_config()
|
||||||
15
vollerei/game/hsr/constants.py
Normal file
15
vollerei/game/hsr/constants.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
MD5SUMS = {
|
||||||
|
"1.0.5": {
|
||||||
|
"cn": {
|
||||||
|
"StarRailBase.dll": "66c42871ce82456967d004ccb2d7cf77",
|
||||||
|
"UnityPlayer.dll": "0c866c44bb3752031a8c12ffe935b26f",
|
||||||
|
},
|
||||||
|
"os": {
|
||||||
|
"StarRailBase.dll": "8aa3790aafa3dd176678392f3f93f435",
|
||||||
|
"UnityPlayer.dll": "f17b9b7f9b8c9cbd211bdff7771a80c2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Patches
|
||||||
|
ASTRA_REPO = "https://notabug.org/mkrsym1/astra"
|
||||||
|
JADEITE_REPO = "https://codeberg.org/mkrsym1/jadeite/"
|
||||||
98
vollerei/game/hsr/functions.py
Normal file
98
vollerei/game/hsr/functions.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
from hashlib import md5
|
||||||
|
from vollerei.common.enums import GameChannel
|
||||||
|
from vollerei.abc.launcher.game import GameABC
|
||||||
|
from vollerei.game.hsr.constants import MD5SUMS
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel(game: GameABC) -> GameChannel | None:
|
||||||
|
"""
|
||||||
|
Gets the current game channel.
|
||||||
|
|
||||||
|
Only works for Star Rail version 1.0.5, other versions will return the
|
||||||
|
overridden channel or GameChannel.Overseas if no channel is overridden.
|
||||||
|
|
||||||
|
This is not needed for game patching, since the patcher will automatically
|
||||||
|
detect the channel.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GameChannel: The current game channel.
|
||||||
|
"""
|
||||||
|
version = game.version_override or game.get_version()
|
||||||
|
if version == (1, 0, 5):
|
||||||
|
for channel, v in MD5SUMS["1.0.5"].values():
|
||||||
|
for file, md5sum in v.values():
|
||||||
|
if md5(game.path.joinpath(file).read_bytes()).hexdigest() != md5sum:
|
||||||
|
continue
|
||||||
|
match channel:
|
||||||
|
case "cn":
|
||||||
|
return GameChannel.China
|
||||||
|
case "os":
|
||||||
|
return GameChannel.Overseas
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(game: GameABC) -> tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Gets the current installed game version.
|
||||||
|
|
||||||
|
Credits to An Anime Team for the code that does the magic:
|
||||||
|
https://github.com/an-anime-team/anime-game-core/blob/main/src/games/star_rail/game.rs#L49
|
||||||
|
|
||||||
|
If the above method fails, it'll fallback to read the config.ini file
|
||||||
|
for the version, which is not recommended (as described in
|
||||||
|
`get_version_config()` docs)
|
||||||
|
|
||||||
|
This returns (0, 0, 0) if the version could not be found
|
||||||
|
(usually indicates the game is not installed), and in fact `is_installed()` uses
|
||||||
|
this method to check if the game is installed too.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[int, int, int]: The version as a tuple of integers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_file = game.data_folder().joinpath("data.unity3d")
|
||||||
|
if not data_file.exists():
|
||||||
|
return game.get_version_config()
|
||||||
|
|
||||||
|
def bytes_to_int(byte_array: list[bytes]) -> int:
|
||||||
|
bytes_as_int = int.from_bytes(byte_array, byteorder="big")
|
||||||
|
actual_int = bytes_as_int - 48 # 48 is the ASCII code for 0
|
||||||
|
return actual_int
|
||||||
|
|
||||||
|
version_bytes: list[list[bytes]] = [[], [], []]
|
||||||
|
version_ptr = 0
|
||||||
|
correct = True
|
||||||
|
try:
|
||||||
|
with data_file.open("rb") as f:
|
||||||
|
f.seek(0x7D0) # 2000 in decimal
|
||||||
|
for byte in f.read(10000):
|
||||||
|
match byte:
|
||||||
|
case 0:
|
||||||
|
version_bytes = [[], [], []]
|
||||||
|
version_ptr = 0
|
||||||
|
correct = True
|
||||||
|
case 46:
|
||||||
|
version_ptr += 1
|
||||||
|
if version_ptr > 2:
|
||||||
|
correct = False
|
||||||
|
case 38:
|
||||||
|
if (
|
||||||
|
correct
|
||||||
|
and len(version_bytes[0]) > 0
|
||||||
|
and len(version_bytes[1]) > 0
|
||||||
|
and len(version_bytes[2]) > 0
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
bytes_to_int(version_bytes[0]),
|
||||||
|
bytes_to_int(version_bytes[1]),
|
||||||
|
bytes_to_int(version_bytes[2]),
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
if correct and byte in b"0123456789":
|
||||||
|
version_bytes[version_ptr].append(byte)
|
||||||
|
else:
|
||||||
|
correct = False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Fallback to config.ini
|
||||||
|
return game.get_version_config()
|
||||||
5
vollerei/game/launcher/__init__.py
Normal file
5
vollerei/game/launcher/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Re-exports
|
||||||
|
from vollerei.game.launcher.manager import Game
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Game"]
|
||||||
32
vollerei/game/launcher/api.py
Normal file
32
vollerei/game/launcher/api.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from vollerei.common.api import get_game_packages, resource
|
||||||
|
from vollerei.common.enums import GameChannel, GameType
|
||||||
|
|
||||||
|
|
||||||
|
def get_game_package(
|
||||||
|
game_type: GameType, channel: GameChannel = GameChannel.Overseas
|
||||||
|
) -> resource.GameInfo:
|
||||||
|
"""
|
||||||
|
Get game package information from the launcher API.
|
||||||
|
|
||||||
|
Doesn't work with HI3 but well, we haven't implemented anything for that game yet.
|
||||||
|
|
||||||
|
Default channel is overseas.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: Game channel to get the resource information from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GameInfo: Game resource information.
|
||||||
|
"""
|
||||||
|
find_str: str
|
||||||
|
match game_type:
|
||||||
|
case GameType.HSR:
|
||||||
|
find_str = "hkrpg"
|
||||||
|
case GameType.Genshin:
|
||||||
|
find_str = "hk4e"
|
||||||
|
case GameType.ZZZ:
|
||||||
|
find_str = "nap"
|
||||||
|
game_packages = get_game_packages(channel=channel)
|
||||||
|
for package in game_packages:
|
||||||
|
if find_str in package.game.biz:
|
||||||
|
return package
|
||||||
0
vollerei/game/launcher/interface.py
Normal file
0
vollerei/game/launcher/interface.py
Normal file
510
vollerei/game/launcher/manager.py
Normal file
510
vollerei/game/launcher/manager.py
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
from configparser import ConfigParser
|
||||||
|
from io import IOBase
|
||||||
|
from os import PathLike
|
||||||
|
from pathlib import Path, PurePath
|
||||||
|
from vollerei.abc.launcher.game import GameABC
|
||||||
|
from vollerei.common import ConfigFile, functions
|
||||||
|
from vollerei.common.api import resource
|
||||||
|
from vollerei.common.enums import GameType, VoicePackLanguage, GameChannel
|
||||||
|
from vollerei.exceptions.game import (
|
||||||
|
GameAlreadyUpdatedError,
|
||||||
|
GameNotInstalledError,
|
||||||
|
PreDownloadNotAvailable,
|
||||||
|
)
|
||||||
|
from vollerei.game.launcher import api
|
||||||
|
from vollerei.game.hsr import functions as hsr_functions
|
||||||
|
from vollerei.game.genshin import functions as genshin_functions
|
||||||
|
from vollerei.game.zzz import functions as zzz_functions
|
||||||
|
from vollerei import paths
|
||||||
|
from vollerei.utils import download
|
||||||
|
|
||||||
|
|
||||||
|
class Game(GameABC):
|
||||||
|
"""
|
||||||
|
Manages the game installation
|
||||||
|
|
||||||
|
For Star Rail and Zenless Zone Zero:
|
||||||
|
|
||||||
|
Since channel detection isn't implemented yet, most functions assume you're
|
||||||
|
using the overseas version of the game. You can override channel by setting
|
||||||
|
the property `channel_override` to the channel you want to use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, game_type: GameType, path: PathLike = None, cache_path: PathLike = None
|
||||||
|
):
|
||||||
|
self._path: Path | None = Path(path) if path else None
|
||||||
|
if not cache_path:
|
||||||
|
cache_path = paths.cache_path
|
||||||
|
cache_path = Path(cache_path)
|
||||||
|
self._game_type = game_type
|
||||||
|
self.cache: Path = cache_path.joinpath(f"game/{self._game_type.name.lower()}/")
|
||||||
|
self.cache.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._version_override: tuple[int, int, int] | None = None
|
||||||
|
self._channel_override: GameChannel | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version_override(self) -> tuple[int, int, int] | None:
|
||||||
|
"""
|
||||||
|
Overrides the game version.
|
||||||
|
|
||||||
|
This can be useful if you want to override the version of the game
|
||||||
|
and additionally working around bugs.
|
||||||
|
"""
|
||||||
|
return self._version_override
|
||||||
|
|
||||||
|
@version_override.setter
|
||||||
|
def version_override(self, version: tuple[int, int, int] | str | None):
|
||||||
|
if isinstance(version, str):
|
||||||
|
version = tuple(int(i) for i in version.split("."))
|
||||||
|
self._version_override = version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel_override(self) -> GameChannel | None:
|
||||||
|
"""
|
||||||
|
Overrides the game channel.
|
||||||
|
|
||||||
|
Because game channel detection isn't implemented yet, you may need
|
||||||
|
to use this for some functions to work.
|
||||||
|
|
||||||
|
This can be useful if you want to override the channel of the game
|
||||||
|
and additionally working around bugs.
|
||||||
|
"""
|
||||||
|
return self._channel_override
|
||||||
|
|
||||||
|
@channel_override.setter
|
||||||
|
def channel_override(self, channel: GameChannel | str | None):
|
||||||
|
if isinstance(channel, str):
|
||||||
|
channel = GameChannel[channel]
|
||||||
|
self._channel_override = channel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self) -> Path | None:
|
||||||
|
"""
|
||||||
|
Paths to the game folder.
|
||||||
|
"""
|
||||||
|
return self._path
|
||||||
|
|
||||||
|
@path.setter
|
||||||
|
def path(self, path: PathLike):
|
||||||
|
self._path = Path(path)
|
||||||
|
|
||||||
|
def data_folder(self) -> Path:
|
||||||
|
"""
|
||||||
|
Paths to the game data folder.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path: The path to the game data folder.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
match self._game_type:
|
||||||
|
case GameType.Genshin:
|
||||||
|
match self.get_channel():
|
||||||
|
case GameChannel.China:
|
||||||
|
return self._path.joinpath("YuanShen_Data")
|
||||||
|
case GameChannel.Overseas:
|
||||||
|
return self._path.joinpath("GenshinImpact_Data")
|
||||||
|
case GameType.HSR:
|
||||||
|
return self._path.joinpath("StarRail_Data")
|
||||||
|
case GameType.ZZZ:
|
||||||
|
return self._path.joinpath("ZenlessZoneZero_Data")
|
||||||
|
except AttributeError:
|
||||||
|
raise GameNotInstalledError("Game path is not set.")
|
||||||
|
|
||||||
|
def is_installed(self) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if the game is installed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the game is installed, False otherwise.
|
||||||
|
"""
|
||||||
|
if self._path is None:
|
||||||
|
return False
|
||||||
|
match self._game_type:
|
||||||
|
case GameType.Genshin:
|
||||||
|
match self.get_channel():
|
||||||
|
case GameChannel.China:
|
||||||
|
if not self._path.joinpath("YuanShen.exe").exists():
|
||||||
|
return False
|
||||||
|
case GameChannel.Overseas:
|
||||||
|
if not self._path.joinpath("GenshinImpact.exe").exists():
|
||||||
|
return False
|
||||||
|
case GameType.HSR:
|
||||||
|
if not self._path.joinpath("StarRail.exe").exists():
|
||||||
|
return False
|
||||||
|
case GameType.ZZZ:
|
||||||
|
if not self._path.joinpath("ZenlessZoneZero.exe").exists():
|
||||||
|
return False
|
||||||
|
if not self.data_folder().is_dir():
|
||||||
|
return False
|
||||||
|
if self.get_version() == (0, 0, 0):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_channel(self) -> GameChannel:
|
||||||
|
"""
|
||||||
|
Gets the current game channel.
|
||||||
|
|
||||||
|
Only works for Genshin and Star Rail version 1.0.5, other versions will return
|
||||||
|
the overridden channel or GameChannel.Overseas if no channel is overridden.
|
||||||
|
|
||||||
|
This is not needed for game patching, since the patcher will automatically
|
||||||
|
detect the channel.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GameChannel: The current game channel.
|
||||||
|
"""
|
||||||
|
match self._game_type:
|
||||||
|
case GameType.HSR:
|
||||||
|
return (
|
||||||
|
hsr_functions.get_channel(self)
|
||||||
|
or self._channel_override
|
||||||
|
or GameChannel.Overseas
|
||||||
|
)
|
||||||
|
case GameType.Genshin:
|
||||||
|
return (
|
||||||
|
genshin_functions.get_channel(self)
|
||||||
|
or self._channel_override
|
||||||
|
or GameChannel.Overseas
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
return self._channel_override or GameChannel.Overseas
|
||||||
|
|
||||||
|
def get_version_config(self) -> tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Gets the current installed game version from config.ini.
|
||||||
|
|
||||||
|
Using this is not recommended, as only official launcher creates
|
||||||
|
and uses this file, instead you should use `get_version()`.
|
||||||
|
|
||||||
|
This returns (0, 0, 0) if the version could not be found.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[int, int, int]: Game version.
|
||||||
|
"""
|
||||||
|
cfg_file = self._path.joinpath("config.ini")
|
||||||
|
if not cfg_file.exists():
|
||||||
|
return (0, 0, 0)
|
||||||
|
cfg = ConfigFile(cfg_file)
|
||||||
|
# Fk u miHoYo
|
||||||
|
if "general" in cfg.sections():
|
||||||
|
version_str = cfg.get("general", "game_version", fallback="0.0.0")
|
||||||
|
elif "General" in cfg.sections():
|
||||||
|
version_str = cfg.get("General", "game_version", fallback="0.0.0")
|
||||||
|
else:
|
||||||
|
return (0, 0, 0)
|
||||||
|
if version_str.count(".") != 2:
|
||||||
|
return (0, 0, 0)
|
||||||
|
try:
|
||||||
|
version = tuple(int(i) for i in version_str.split("."))
|
||||||
|
except Exception:
|
||||||
|
return (0, 0, 0)
|
||||||
|
return version
|
||||||
|
|
||||||
|
def set_version_config(self):
|
||||||
|
"""
|
||||||
|
Sets the current installed game version to config.ini.
|
||||||
|
|
||||||
|
Only works for the global version of the game, not the Chinese one (since I don't have
|
||||||
|
them installed to test).
|
||||||
|
|
||||||
|
This method is meant to keep compatibility with the official launcher only.
|
||||||
|
"""
|
||||||
|
cfg_file = self._path.joinpath("config.ini")
|
||||||
|
if cfg_file.exists():
|
||||||
|
cfg = ConfigFile(cfg_file)
|
||||||
|
cfg.set("general", "game_version", self.get_version_str())
|
||||||
|
cfg.save()
|
||||||
|
else:
|
||||||
|
cfg_dict = {
|
||||||
|
"general": {
|
||||||
|
"channel": 1,
|
||||||
|
"cps": "hyp_hoyoverse",
|
||||||
|
"game_version": self.get_version_str(),
|
||||||
|
"sub_channel": 0,
|
||||||
|
# This probably should be fetched from the server but well
|
||||||
|
"plugin_n06mjyc2r3_version": "1.1.0",
|
||||||
|
"uapc": None, # Honestly what's this?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self._game_type:
|
||||||
|
case GameType.Genshin:
|
||||||
|
cfg_dict["general"]["uapc"] = {
|
||||||
|
"hk4e_global": {"uapc": "f55586a8ce9f_"},
|
||||||
|
"hyp": {"uapc": "f55586a8ce9f_"},
|
||||||
|
}
|
||||||
|
case GameType.HSR:
|
||||||
|
cfg_dict["general"]["uapc"] = {
|
||||||
|
"hkrpg_global": {"uapc": "f5c7c6262812_"},
|
||||||
|
"hyp": {"uapc": "f55586a8ce9f_"},
|
||||||
|
}
|
||||||
|
case GameType.ZZZ:
|
||||||
|
cfg_dict["general"]["uapc"] = {
|
||||||
|
"nap_global": {"uapc": "f55586a8ce9f_"},
|
||||||
|
"hyp": {"uapc": "f55586a8ce9f_"},
|
||||||
|
}
|
||||||
|
cfg = ConfigParser()
|
||||||
|
cfg.read_dict(cfg_dict)
|
||||||
|
cfg.write(cfg_file.open("w"))
|
||||||
|
|
||||||
|
def get_version(self) -> tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Gets the current installed game version.
|
||||||
|
|
||||||
|
Credits to An Anime Team for the code that does the magic, see the source
|
||||||
|
in `hsr/functions.py`, `genshin/functions.py` and `zzz/functions.py` for more info
|
||||||
|
|
||||||
|
If the above method fails, it'll fallback to read the config.ini file
|
||||||
|
for the version, which is not recommended (as described in
|
||||||
|
`get_version_config()` docs)
|
||||||
|
|
||||||
|
This returns (0, 0, 0) if the version could not be found
|
||||||
|
(usually indicates the game is not installed), and in fact `is_installed()` uses
|
||||||
|
this method to check if the game is installed too.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[int, int, int]: The version as a tuple of integers.
|
||||||
|
"""
|
||||||
|
match self._game_type:
|
||||||
|
case GameType.HSR:
|
||||||
|
return hsr_functions.get_version(self)
|
||||||
|
case GameType.Genshin:
|
||||||
|
return genshin_functions.get_version(self)
|
||||||
|
case GameType.ZZZ:
|
||||||
|
return zzz_functions.get_version(self)
|
||||||
|
case _:
|
||||||
|
return self.get_version_config()
|
||||||
|
|
||||||
|
def get_version_str(self) -> str:
|
||||||
|
"""
|
||||||
|
Gets the current installed game version as a string.
|
||||||
|
|
||||||
|
Because this method uses `get_version()`, you should read the docs of
|
||||||
|
that method too.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The version as a string.
|
||||||
|
"""
|
||||||
|
return ".".join(str(i) for i in self.get_version())
|
||||||
|
|
||||||
|
def get_installed_voicepacks(self) -> list[VoicePackLanguage]:
|
||||||
|
"""
|
||||||
|
Gets the installed voicepacks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[VoicePackLanguage]: A list of installed voicepacks.
|
||||||
|
"""
|
||||||
|
if not self.is_installed():
|
||||||
|
raise GameNotInstalledError("Game is not installed.")
|
||||||
|
voicepacks = []
|
||||||
|
blacklisted_words = ["SFX"]
|
||||||
|
audio_package: Path
|
||||||
|
match self._game_type:
|
||||||
|
case GameType.Genshin:
|
||||||
|
audio_package = self.data_folder().joinpath(
|
||||||
|
"StreamingAssets/AudioAssets/"
|
||||||
|
)
|
||||||
|
if audio_package.joinpath("AudioPackage").is_dir():
|
||||||
|
audio_package = audio_package.joinpath("AudioPackage")
|
||||||
|
case GameType.HSR:
|
||||||
|
audio_package = self.data_folder().joinpath(
|
||||||
|
"Persistent/Audio/AudioPackage/Windows/"
|
||||||
|
)
|
||||||
|
case GameType.ZZZ:
|
||||||
|
audio_package = self.data_folder().joinpath(
|
||||||
|
"StreamingAssets/Audio/Windows/Full/"
|
||||||
|
)
|
||||||
|
for child in audio_package.iterdir():
|
||||||
|
if child.resolve().is_dir() and child.name not in blacklisted_words:
|
||||||
|
name = child.name
|
||||||
|
if name.startswith("English"):
|
||||||
|
name = "English"
|
||||||
|
voicepack: VoicePackLanguage
|
||||||
|
try:
|
||||||
|
if self._game_type == GameType.ZZZ:
|
||||||
|
voicepack = VoicePackLanguage.from_zzz_name(child.name)
|
||||||
|
else:
|
||||||
|
voicepack = VoicePackLanguage[name]
|
||||||
|
voicepacks.append(voicepack)
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
pass
|
||||||
|
return voicepacks
|
||||||
|
|
||||||
|
def get_remote_game(
|
||||||
|
self, pre_download: bool = False
|
||||||
|
) -> resource.Main | resource.PreDownload:
|
||||||
|
"""
|
||||||
|
Gets the current game information from remote.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pre_download (bool): Whether to get the pre-download version.
|
||||||
|
Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A `Main` or `PreDownload` object that contains the game information.
|
||||||
|
"""
|
||||||
|
channel = self._channel_override or self.get_channel()
|
||||||
|
if pre_download:
|
||||||
|
game = api.get_game_package(
|
||||||
|
game_type=self._game_type, channel=channel
|
||||||
|
).pre_download
|
||||||
|
if not game:
|
||||||
|
raise PreDownloadNotAvailable("Pre-download version is not available.")
|
||||||
|
return game
|
||||||
|
return api.get_game_package(game_type=self._game_type, channel=channel).main
|
||||||
|
|
||||||
|
def get_update(self, pre_download: bool = False) -> resource.Patch | None:
|
||||||
|
"""
|
||||||
|
Gets the current game update.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pre_download (bool): Whether to get the pre-download version.
|
||||||
|
Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A `Patch` object that contains the update information or
|
||||||
|
`None` if the game is not installed or already up-to-date.
|
||||||
|
"""
|
||||||
|
if not self.is_installed():
|
||||||
|
return None
|
||||||
|
version = (
|
||||||
|
".".join(str(x) for x in self._version_override)
|
||||||
|
if self._version_override
|
||||||
|
else self.get_version_str()
|
||||||
|
)
|
||||||
|
for patch in self.get_remote_game(pre_download=pre_download).patches:
|
||||||
|
if patch.version == version:
|
||||||
|
return patch
|
||||||
|
return None
|
||||||
|
|
||||||
|
def repair_file(
|
||||||
|
self,
|
||||||
|
file: PathLike,
|
||||||
|
pre_download: bool = False,
|
||||||
|
game_info: resource.Game = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Repairs a game file.
|
||||||
|
|
||||||
|
This will automatically handle backup and restore the file if the repair
|
||||||
|
fails.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file (PathLike): The file to repair.
|
||||||
|
pre_download (bool): Whether to get the pre-download version.
|
||||||
|
Defaults to False.
|
||||||
|
"""
|
||||||
|
return self.repair_files([file], pre_download=pre_download, game_info=game_info)
|
||||||
|
|
||||||
|
def repair_files(
|
||||||
|
self,
|
||||||
|
files: list[PathLike],
|
||||||
|
pre_download: bool = False,
|
||||||
|
game_info: resource.Game = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Repairs multiple game files.
|
||||||
|
|
||||||
|
This will automatically handle backup and restore the file if the repair
|
||||||
|
fails.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files (PathLike): The files to repair.
|
||||||
|
pre_download (bool): Whether to get the pre-download version.
|
||||||
|
Defaults to False.
|
||||||
|
game_info (resource.Game): The game information to use for repair.
|
||||||
|
"""
|
||||||
|
functions.repair_files(
|
||||||
|
self, files, pre_download=pre_download, game_info=game_info
|
||||||
|
)
|
||||||
|
|
||||||
|
def repair_game(self) -> None:
|
||||||
|
"""
|
||||||
|
Tries to repair the game by reading "pkg_version" file and downloading the
|
||||||
|
mismatched files from the server.
|
||||||
|
"""
|
||||||
|
functions.repair_game(self)
|
||||||
|
|
||||||
|
def install_archive(self, archive_file: PathLike | IOBase) -> None:
|
||||||
|
"""
|
||||||
|
Applies an install archive to the game, it can be the game itself or a
|
||||||
|
voicepack one.
|
||||||
|
|
||||||
|
`archive_file` can be a path to the archive file or a file-like object,
|
||||||
|
like if you have very high amount of RAM and want to download the archive
|
||||||
|
to memory instead of disk, this can be useful for you.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
archive_file (PathLike | IOBase): The archive file.
|
||||||
|
"""
|
||||||
|
if not isinstance(archive_file, IOBase):
|
||||||
|
archive_file = Path(archive_file)
|
||||||
|
functions.install_archive(self, archive_file)
|
||||||
|
|
||||||
|
def apply_update_archive(
|
||||||
|
self, archive_file: PathLike | IOBase, auto_repair: bool = True
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Applies an update archive to the game, it can be the game update or a
|
||||||
|
voicepack update.
|
||||||
|
|
||||||
|
`archive_file` can be a path to the archive file or a file-like object,
|
||||||
|
like if you have very high amount of RAM and want to download the update
|
||||||
|
to memory instead of disk, this can be useful for you.
|
||||||
|
|
||||||
|
`auto_repair` is used to determine whether to repair the file if it's
|
||||||
|
broken. If it's set to False, then it'll raise an exception if the file
|
||||||
|
is broken.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
archive_file (PathLike | IOBase): The archive file.
|
||||||
|
auto_repair (bool, optional): Whether to repair the file if it's broken.
|
||||||
|
Defaults to True.
|
||||||
|
"""
|
||||||
|
if not self.is_installed():
|
||||||
|
raise GameNotInstalledError("Game is not installed.")
|
||||||
|
if not isinstance(archive_file, IOBase):
|
||||||
|
archive_file = Path(archive_file)
|
||||||
|
# Hello hell again, dealing with HDiffPatch and all the things again.
|
||||||
|
functions.apply_update_archive(self, archive_file, auto_repair=auto_repair)
|
||||||
|
|
||||||
|
def install_update(
|
||||||
|
self, update_info: resource.Patch = None, auto_repair: bool = True
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Installs an update from a `Patch` object.
|
||||||
|
|
||||||
|
You may want to download the update manually and pass it to
|
||||||
|
`apply_update_archive()` instead for better control, and after that
|
||||||
|
execute `set_version_config()` to set the game version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
update_info (Diff, optional): The update information. Defaults to None.
|
||||||
|
auto_repair (bool, optional): Whether to repair the file if it's broken.
|
||||||
|
Defaults to True.
|
||||||
|
"""
|
||||||
|
if not self.is_installed():
|
||||||
|
raise GameNotInstalledError("Game is not installed.")
|
||||||
|
if not update_info:
|
||||||
|
update_info = self.get_update()
|
||||||
|
if not update_info or update_info.version == self.get_version_str():
|
||||||
|
raise GameAlreadyUpdatedError("Game is already updated.")
|
||||||
|
update_url = update_info.game_pkgs[0].url
|
||||||
|
# Base game update
|
||||||
|
archive_file = self.cache.joinpath(PurePath(update_url).name)
|
||||||
|
download(update_url, archive_file)
|
||||||
|
self.apply_update_archive(archive_file=archive_file, auto_repair=auto_repair)
|
||||||
|
# Get installed voicepacks
|
||||||
|
installed_voicepacks = self.get_installed_voicepacks()
|
||||||
|
# Voicepack update
|
||||||
|
for remote_voicepack in update_info.audio_pkgs:
|
||||||
|
if remote_voicepack.language not in installed_voicepacks:
|
||||||
|
continue
|
||||||
|
# Voicepack is installed, update it
|
||||||
|
archive_file = self.cache.joinpath(PurePath(remote_voicepack.url).name)
|
||||||
|
download(remote_voicepack.url, archive_file)
|
||||||
|
self.apply_update_archive(
|
||||||
|
archive_file=archive_file, auto_repair=auto_repair
|
||||||
|
)
|
||||||
|
self.set_version_config()
|
||||||
223
vollerei/game/patcher.py
Normal file
223
vollerei/game/patcher.py
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from shutil import copy2, rmtree
|
||||||
|
from packaging import version
|
||||||
|
from vollerei.abc.patcher import PatcherABC
|
||||||
|
from vollerei.common import telemetry
|
||||||
|
from vollerei.exceptions.game import GameNotInstalledError
|
||||||
|
from vollerei.exceptions.patcher import (
|
||||||
|
VersionNotSupportedError,
|
||||||
|
PatcherError,
|
||||||
|
PatchUpdateError,
|
||||||
|
)
|
||||||
|
from vollerei.game.launcher.manager import Game, GameChannel
|
||||||
|
from vollerei.utils import download_and_extract, Git, Xdelta3
|
||||||
|
from vollerei.paths import tools_data_path
|
||||||
|
from vollerei.hsr.constants import ASTRA_REPO, JADEITE_REPO
|
||||||
|
|
||||||
|
|
||||||
|
class PatchType(Enum):
|
||||||
|
"""
|
||||||
|
Patch type
|
||||||
|
|
||||||
|
Astra: The old patch which patch the game directly (not recommended).
|
||||||
|
Jadeite: The new patch which patch the game in memory by DLL injection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Astra = 0
|
||||||
|
Jadeite = 1
|
||||||
|
|
||||||
|
|
||||||
|
class Patcher(PatcherABC):
|
||||||
|
"""
|
||||||
|
Patch helper for HSR and HI3.
|
||||||
|
|
||||||
|
By default this will use Jadeite as it is maintained and more stable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Patch type, can be either Astra or Jadeite
|
||||||
|
"""
|
||||||
|
return self._patch_type
|
||||||
|
|
||||||
|
@patch_type.setter
|
||||||
|
def patch_type(self, value: PatchType):
|
||||||
|
self._patch_type = value
|
||||||
|
|
||||||
|
def _update_astra(self):
|
||||||
|
self._git.pull_or_clone(ASTRA_REPO, self._astra)
|
||||||
|
|
||||||
|
def _update_jadeite(self):
|
||||||
|
release_info = self._git.get_latest_release(JADEITE_REPO)
|
||||||
|
file = self._git.get_latest_release_dl(release_info)[0]
|
||||||
|
file_version = release_info["tag_name"][1:] # Remove "v" prefix
|
||||||
|
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 version.parse(file_version) <= version.parse(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):
|
||||||
|
"""
|
||||||
|
Update the patch
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
match self._patch_type:
|
||||||
|
case PatchType.Astra:
|
||||||
|
self._update_astra()
|
||||||
|
case PatchType.Jadeite:
|
||||||
|
self._update_jadeite()
|
||||||
|
except Exception as e:
|
||||||
|
raise PatchUpdateError("Failed to update patch.") from e
|
||||||
|
|
||||||
|
def _patch_astra(self, game: Game):
|
||||||
|
if game.get_version() != (1, 0, 5):
|
||||||
|
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 and patch
|
||||||
|
for file in ["UnityPlayer.dll", "StarRailBase.dll"]:
|
||||||
|
game.path.joinpath(file).rename(game.path.joinpath(f"{file}.bak"))
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
"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 _unpatch_astra(self, game: Game):
|
||||||
|
if game.get_version() != (1, 0, 5):
|
||||||
|
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"
|
||||||
|
# Restore
|
||||||
|
for file in ["UnityPlayer.dll", "StarRailBase.dll"]:
|
||||||
|
if game.path.joinpath(f"{file}.bak").exists():
|
||||||
|
game.path.joinpath(file).unlink()
|
||||||
|
game.path.joinpath(f"{file}.bak").rename(game.path.joinpath(file))
|
||||||
|
# Remove files
|
||||||
|
for file in self._astra.joinpath(f"{file_type}/files/").rglob("*"):
|
||||||
|
if file.suffix == ".bat":
|
||||||
|
continue
|
||||||
|
file_rel = file.relative_to(self._astra.joinpath(f"{file_type}/files/"))
|
||||||
|
game_path = game.path.joinpath(file_rel)
|
||||||
|
if game_path.is_file():
|
||||||
|
game_path.unlink()
|
||||||
|
elif game_path.is_dir():
|
||||||
|
try:
|
||||||
|
game_path.rmdir()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _unpatch_jadeite(self):
|
||||||
|
rmtree(self._jadeite, ignore_errors=True)
|
||||||
|
|
||||||
|
def patch_game(self, game: Game):
|
||||||
|
"""
|
||||||
|
Patch the game
|
||||||
|
|
||||||
|
If you use Jadeite (by default), this will just download Jadeite files
|
||||||
|
and won't actually patch the game because Jadeite will do that automatically.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
game (Game): The game to patch
|
||||||
|
"""
|
||||||
|
if not game.is_installed():
|
||||||
|
raise PatcherError(GameNotInstalledError("Game is not installed"))
|
||||||
|
match self._patch_type:
|
||||||
|
case PatchType.Astra:
|
||||||
|
self._patch_astra(game)
|
||||||
|
case PatchType.Jadeite:
|
||||||
|
return self._patch_jadeite()
|
||||||
|
|
||||||
|
def unpatch_game(self, game: Game):
|
||||||
|
"""
|
||||||
|
Unpatch the game
|
||||||
|
|
||||||
|
If you use Jadeite (by default), this will just delete Jadeite files.
|
||||||
|
Note that Honkai Impact 3rd uses Jadeite too, so executing this will
|
||||||
|
delete the files needed by both games.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
game (Game): The game to unpatch
|
||||||
|
"""
|
||||||
|
if not game.is_installed():
|
||||||
|
raise PatcherError(GameNotInstalledError("Game is not installed"))
|
||||||
|
match self._patch_type:
|
||||||
|
case PatchType.Astra:
|
||||||
|
self._unpatch_astra(game)
|
||||||
|
case PatchType.Jadeite:
|
||||||
|
self._unpatch_jadeite()
|
||||||
|
|
||||||
|
def check_telemetry(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
Check if telemetry servers are accessible by the user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: A list of telemetry servers that are accessible
|
||||||
|
"""
|
||||||
|
return telemetry.check_telemetry()
|
||||||
|
|
||||||
|
def block_telemetry(self, telemetry_list: list[str] = None):
|
||||||
|
"""
|
||||||
|
Block the telemetry servers
|
||||||
|
|
||||||
|
If telemetry_list is not provided, it will be checked automatically.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
telemetry_list (list[str], optional): A list of telemetry servers to block.
|
||||||
|
"""
|
||||||
|
telemetry.block_telemetry(telemetry_list)
|
||||||
66
vollerei/game/zzz/functions.py
Normal file
66
vollerei/game/zzz/functions.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from vollerei.abc.launcher.game import GameABC
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(game: GameABC) -> tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Gets the current installed game version.
|
||||||
|
|
||||||
|
Credits to An Anime Team for the code that does the magic:
|
||||||
|
https://github.com/an-anime-team/anime-game-core/blob/main/src/games/zzz/game.rs#L49
|
||||||
|
|
||||||
|
If the above method fails, it'll fallback to read the config.ini file
|
||||||
|
for the version, which is not recommended (as described in
|
||||||
|
`get_version_config()` docs)
|
||||||
|
|
||||||
|
This returns (0, 0, 0) if the version could not be found
|
||||||
|
(usually indicates the game is not installed), and in fact `is_installed()` uses
|
||||||
|
this method to check if the game is installed too.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[int, int, int]: The version as a tuple of integers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_file = game.data_folder().joinpath("globalgamemanagers")
|
||||||
|
if not data_file.exists():
|
||||||
|
return game.get_version_config()
|
||||||
|
|
||||||
|
def bytes_to_int(byte_array: list[bytes]) -> int:
|
||||||
|
bytes_as_int = int.from_bytes(byte_array, byteorder="big")
|
||||||
|
actual_int = bytes_as_int - 48 # 48 is the ASCII code for 0
|
||||||
|
return actual_int
|
||||||
|
|
||||||
|
version_bytes: list[list[bytes]] = [[], [], []]
|
||||||
|
version_ptr = 0
|
||||||
|
correct = True
|
||||||
|
try:
|
||||||
|
with data_file.open("rb") as f:
|
||||||
|
f.seek(4000)
|
||||||
|
for byte in f.read(10000):
|
||||||
|
match byte:
|
||||||
|
case 0:
|
||||||
|
if (
|
||||||
|
correct
|
||||||
|
and len(version_bytes[0]) > 0
|
||||||
|
and len(version_bytes[1]) > 0
|
||||||
|
and len(version_bytes[2]) > 0
|
||||||
|
):
|
||||||
|
found_version = tuple(
|
||||||
|
bytes_to_int(i) for i in version_bytes
|
||||||
|
)
|
||||||
|
return found_version
|
||||||
|
version_bytes = [[], [], []]
|
||||||
|
version_ptr = 0
|
||||||
|
correct = True
|
||||||
|
case b".":
|
||||||
|
version_ptr += 1
|
||||||
|
if version_ptr > 2:
|
||||||
|
correct = False
|
||||||
|
case _:
|
||||||
|
if correct and byte in b"0123456789":
|
||||||
|
version_bytes[version_ptr].append(byte)
|
||||||
|
else:
|
||||||
|
correct = False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Fallback to config.ini
|
||||||
|
return game.get_version_config()
|
||||||
@ -1,5 +1,5 @@
|
|||||||
# Re-exports
|
# Re-exports
|
||||||
from vollerei.genshin.launcher import Game, GameChannel
|
from vollerei.genshin.launcher import Game
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Game", "GameChannel"]
|
__all__ = ["Game"]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# Re-exports
|
# Re-exports
|
||||||
from vollerei.genshin.launcher.game import Game, GameChannel
|
from vollerei.genshin.launcher.game import Game
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Game", "GameChannel"]
|
__all__ = ["Game"]
|
||||||
|
|||||||
@ -1,445 +1,12 @@
|
|||||||
from configparser import ConfigParser
|
|
||||||
from io import IOBase
|
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path, PurePath
|
from vollerei.game.launcher.manager import Game as CommonGame
|
||||||
from vollerei.abc.launcher.game import GameABC
|
from vollerei.common.enums import GameType
|
||||||
from vollerei.common import ConfigFile, functions
|
|
||||||
from vollerei.common.api import resource
|
|
||||||
from vollerei.common.enums import VoicePackLanguage, GameChannel
|
|
||||||
from vollerei.exceptions.game import (
|
|
||||||
GameAlreadyUpdatedError,
|
|
||||||
GameNotInstalledError,
|
|
||||||
PreDownloadNotAvailable,
|
|
||||||
)
|
|
||||||
from vollerei.genshin.launcher import api
|
|
||||||
from vollerei import paths
|
|
||||||
from vollerei.utils import download
|
|
||||||
|
|
||||||
|
|
||||||
class Game(GameABC):
|
class Game(CommonGame):
|
||||||
"""
|
"""
|
||||||
Manages the game installation
|
Manages the game installation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path: PathLike = None, cache_path: PathLike = None):
|
def __init__(self, path: PathLike = None, cache_path: PathLike = None):
|
||||||
self._path: Path | None = Path(path) if path else None
|
super().__init__(GameType.Genshin, path, cache_path)
|
||||||
if not cache_path:
|
|
||||||
cache_path = paths.cache_path
|
|
||||||
cache_path = Path(cache_path)
|
|
||||||
self.cache: Path = cache_path.joinpath("game/genshin/")
|
|
||||||
self.cache.mkdir(parents=True, exist_ok=True)
|
|
||||||
self._version_override: tuple[int, int, int] | None = None
|
|
||||||
self._channel_override: GameChannel | None = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version_override(self) -> tuple[int, int, int] | None:
|
|
||||||
"""
|
|
||||||
Overrides the game version.
|
|
||||||
|
|
||||||
This can be useful if you want to override the version of the game
|
|
||||||
and additionally working around bugs.
|
|
||||||
"""
|
|
||||||
return self._version_override
|
|
||||||
|
|
||||||
@version_override.setter
|
|
||||||
def version_override(self, version: tuple[int, int, int] | str | None):
|
|
||||||
if isinstance(version, str):
|
|
||||||
version = tuple(int(i) for i in version.split("."))
|
|
||||||
self._version_override = version
|
|
||||||
|
|
||||||
@property
|
|
||||||
def channel_override(self) -> GameChannel | None:
|
|
||||||
"""
|
|
||||||
Overrides the game channel.
|
|
||||||
|
|
||||||
This can be useful if you want to override the channel of the game
|
|
||||||
and additionally working around bugs.
|
|
||||||
"""
|
|
||||||
return self._channel_override
|
|
||||||
|
|
||||||
@channel_override.setter
|
|
||||||
def channel_override(self, channel: GameChannel | str | None):
|
|
||||||
if isinstance(channel, str):
|
|
||||||
channel = GameChannel[channel]
|
|
||||||
self._channel_override = channel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self) -> Path | None:
|
|
||||||
"""
|
|
||||||
Paths to the game folder.
|
|
||||||
"""
|
|
||||||
return self._path
|
|
||||||
|
|
||||||
@path.setter
|
|
||||||
def path(self, path: PathLike):
|
|
||||||
self._path = Path(path)
|
|
||||||
|
|
||||||
def data_folder(self) -> Path:
|
|
||||||
"""
|
|
||||||
Paths to the game data folder.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
match self.get_channel():
|
|
||||||
case GameChannel.China:
|
|
||||||
return self._path.joinpath("YuanShen_Data")
|
|
||||||
case GameChannel.Overseas:
|
|
||||||
return self._path.joinpath("GenshinImpact_Data")
|
|
||||||
except AttributeError:
|
|
||||||
raise GameNotInstalledError("Game path is not set.")
|
|
||||||
|
|
||||||
def is_installed(self) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if the game is installed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the game is installed, False otherwise.
|
|
||||||
"""
|
|
||||||
if self._path is None:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
self.get_channel()
|
|
||||||
if self.data_folder().is_dir():
|
|
||||||
return True
|
|
||||||
except GameNotInstalledError:
|
|
||||||
return False
|
|
||||||
if self.get_version() == (0, 0, 0):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_channel(self) -> GameChannel:
|
|
||||||
"""
|
|
||||||
Gets the current game channel.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
GameChannel: The current game channel.
|
|
||||||
"""
|
|
||||||
if self._channel_override:
|
|
||||||
return self._channel_override
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game path is not set.")
|
|
||||||
if self._path.joinpath("YuanShen.exe").is_file():
|
|
||||||
return GameChannel.China
|
|
||||||
return GameChannel.Overseas
|
|
||||||
|
|
||||||
def get_version_config(self) -> tuple[int, int, int]:
|
|
||||||
"""
|
|
||||||
Gets the current installed game version from config.ini.
|
|
||||||
|
|
||||||
Using this is not recommended, as only official launcher creates
|
|
||||||
and uses this file, instead you should use `get_version()`.
|
|
||||||
|
|
||||||
This returns (0, 0, 0) if the version could not be found.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[int, int, int]: Game version.
|
|
||||||
"""
|
|
||||||
cfg_file = self._path.joinpath("config.ini")
|
|
||||||
if not cfg_file.exists():
|
|
||||||
return (0, 0, 0)
|
|
||||||
cfg = ConfigFile(cfg_file)
|
|
||||||
# Fk u miHoYo
|
|
||||||
if "general" in cfg.sections():
|
|
||||||
version_str = cfg.get("general", "game_version", fallback="0.0.0")
|
|
||||||
elif "General" in cfg.sections():
|
|
||||||
version_str = cfg.get("General", "game_version", fallback="0.0.0")
|
|
||||||
else:
|
|
||||||
return (0, 0, 0)
|
|
||||||
if version_str.count(".") != 2:
|
|
||||||
return (0, 0, 0)
|
|
||||||
try:
|
|
||||||
version = tuple(int(i) for i in version_str.split("."))
|
|
||||||
except Exception:
|
|
||||||
return (0, 0, 0)
|
|
||||||
return version
|
|
||||||
|
|
||||||
def set_version_config(self):
|
|
||||||
"""
|
|
||||||
Sets the current installed game version to config.ini.
|
|
||||||
|
|
||||||
This method is meant to keep compatibility with the official launcher only.
|
|
||||||
"""
|
|
||||||
cfg_file = self._path.joinpath("config.ini")
|
|
||||||
if cfg_file.exists():
|
|
||||||
cfg = ConfigFile(cfg_file)
|
|
||||||
cfg.set("general", "game_version", self.get_version_str())
|
|
||||||
cfg.save()
|
|
||||||
else:
|
|
||||||
cfg = ConfigParser()
|
|
||||||
cfg.read_dict(
|
|
||||||
{
|
|
||||||
"general": {
|
|
||||||
"downloading_mode": None,
|
|
||||||
"channel": 1,
|
|
||||||
"cps": "hyp_hoyoverse",
|
|
||||||
"game_version": self.get_version_str(),
|
|
||||||
"sub_channel": 0,
|
|
||||||
# This is probably should be fetched from the server but well
|
|
||||||
"plugin_vt8u0pl2cc_version": "1.1.0",
|
|
||||||
# What's this in the Chinese version?
|
|
||||||
"uapc": {
|
|
||||||
"hk4e_global": {"uapc": "f55586a8ce9f_"},
|
|
||||||
"hyp": {"uapc": "f55586a8ce9f_"},
|
|
||||||
}, # Honestly what's this?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cfg.write(cfg_file.open("w"))
|
|
||||||
|
|
||||||
def get_version(self) -> tuple[int, int, int]:
|
|
||||||
"""
|
|
||||||
Gets the current installed game version.
|
|
||||||
|
|
||||||
Credits to An Anime Team for the code that does the magic:
|
|
||||||
https://github.com/an-anime-team/anime-game-core/blob/main/src/games/genshin/game.rs#L52
|
|
||||||
|
|
||||||
If the above method fails, it'll fallback to read the config.ini file
|
|
||||||
for the version, which is not recommended (as described in
|
|
||||||
`get_version_config()` docs)
|
|
||||||
|
|
||||||
This returns (0, 0, 0) if the version could not be found
|
|
||||||
(usually indicates the game is not installed), and in fact `is_installed()` uses
|
|
||||||
this method to check if the game is installed too.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[int, int, int]: The version as a tuple of integers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data_file = self.data_folder().joinpath("globalgamemanagers")
|
|
||||||
if not data_file.exists():
|
|
||||||
return self.get_version_config()
|
|
||||||
|
|
||||||
def bytes_to_int(byte_array: list[bytes]) -> int:
|
|
||||||
bytes_as_int = int.from_bytes(byte_array, byteorder="big")
|
|
||||||
actual_int = bytes_as_int - 48 # 48 is the ASCII code for 0
|
|
||||||
return actual_int
|
|
||||||
|
|
||||||
version_bytes: list[list[bytes]] = [[], [], []]
|
|
||||||
version_ptr = 0
|
|
||||||
correct = True
|
|
||||||
try:
|
|
||||||
with data_file.open("rb") as f:
|
|
||||||
f.seek(4000)
|
|
||||||
for byte in f.read(10000):
|
|
||||||
match byte:
|
|
||||||
case 0:
|
|
||||||
version_bytes = [[], [], []]
|
|
||||||
version_ptr = 0
|
|
||||||
correct = True
|
|
||||||
case 46:
|
|
||||||
version_ptr += 1
|
|
||||||
if version_ptr > 2:
|
|
||||||
correct = False
|
|
||||||
case 95:
|
|
||||||
if (
|
|
||||||
correct
|
|
||||||
and len(version_bytes[0]) > 0
|
|
||||||
and len(version_bytes[1]) > 0
|
|
||||||
and len(version_bytes[2]) > 0
|
|
||||||
):
|
|
||||||
return (
|
|
||||||
bytes_to_int(version_bytes[0]),
|
|
||||||
bytes_to_int(version_bytes[1]),
|
|
||||||
bytes_to_int(version_bytes[2]),
|
|
||||||
)
|
|
||||||
case _:
|
|
||||||
if correct and byte in b"0123456789":
|
|
||||||
version_bytes[version_ptr].append(byte)
|
|
||||||
else:
|
|
||||||
correct = False
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# Fallback to config.ini
|
|
||||||
return self.get_version_config()
|
|
||||||
|
|
||||||
def get_version_str(self) -> str:
|
|
||||||
"""
|
|
||||||
Gets the current installed game version as a string.
|
|
||||||
|
|
||||||
Because this method uses `get_version()`, you should read the docs of
|
|
||||||
that method too.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The version as a string.
|
|
||||||
"""
|
|
||||||
return ".".join(str(i) for i in self.get_version())
|
|
||||||
|
|
||||||
def get_installed_voicepacks(self) -> list[VoicePackLanguage]:
|
|
||||||
"""
|
|
||||||
Gets the installed voicepacks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[VoicePackLanguage]: A list of installed voicepacks.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game is not installed.")
|
|
||||||
voicepacks = []
|
|
||||||
for child in (
|
|
||||||
self.data_folder()
|
|
||||||
.joinpath("StreamingAssets/AudioAssets/AudioPackage/")
|
|
||||||
.iterdir()
|
|
||||||
):
|
|
||||||
if child.resolve().is_dir():
|
|
||||||
try:
|
|
||||||
voicepacks.append(VoicePackLanguage[child.name])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return voicepacks
|
|
||||||
|
|
||||||
def get_remote_game(
|
|
||||||
self, pre_download: bool = False
|
|
||||||
) -> resource.Main | resource.PreDownload:
|
|
||||||
"""
|
|
||||||
Gets the current game information from remote.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A `Main` or `PreDownload` object that contains the game information.
|
|
||||||
"""
|
|
||||||
channel = self._channel_override or self.get_channel()
|
|
||||||
if pre_download:
|
|
||||||
game = api.get_game_package(channel=channel).pre_download
|
|
||||||
if not game:
|
|
||||||
raise PreDownloadNotAvailable("Pre-download version is not available.")
|
|
||||||
return game
|
|
||||||
return api.get_game_package(channel=channel).main
|
|
||||||
|
|
||||||
def get_update(self, pre_download: bool = False) -> resource.Patch | None:
|
|
||||||
"""
|
|
||||||
Gets the current game update.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A `Patch` object that contains the update information or
|
|
||||||
`None` if the game is not installed or already up-to-date.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
return None
|
|
||||||
version = (
|
|
||||||
".".join(str(x) for x in self._version_override)
|
|
||||||
if self._version_override
|
|
||||||
else self.get_version_str()
|
|
||||||
)
|
|
||||||
for patch in self.get_remote_game(pre_download=pre_download).patches:
|
|
||||||
if patch.version == version:
|
|
||||||
return patch
|
|
||||||
return None
|
|
||||||
|
|
||||||
def repair_file(
|
|
||||||
self,
|
|
||||||
file: PathLike,
|
|
||||||
pre_download: bool = False,
|
|
||||||
game_info: resource.Game = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Repairs a game file.
|
|
||||||
|
|
||||||
This will automatically handle backup and restore the file if the repair
|
|
||||||
fails.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file (PathLike): The file to repair.
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
"""
|
|
||||||
return self.repair_files([file], pre_download=pre_download, game_info=game_info)
|
|
||||||
|
|
||||||
def repair_files(
|
|
||||||
self,
|
|
||||||
files: list[PathLike],
|
|
||||||
pre_download: bool = False,
|
|
||||||
game_info: resource.Game = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Repairs multiple game files.
|
|
||||||
|
|
||||||
This will automatically handle backup and restore the file if the repair
|
|
||||||
fails.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
files (PathLike): The files to repair.
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
"""
|
|
||||||
functions.repair_files(
|
|
||||||
self, files, pre_download=pre_download, game_info=game_info
|
|
||||||
)
|
|
||||||
|
|
||||||
def repair_game(self) -> None:
|
|
||||||
"""
|
|
||||||
Tries to repair the game by reading "pkg_version" file and downloading the
|
|
||||||
mismatched files from the server.
|
|
||||||
"""
|
|
||||||
functions.repair_game(self)
|
|
||||||
|
|
||||||
def apply_update_archive(
|
|
||||||
self, archive_file: PathLike | IOBase, auto_repair: bool = True
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Applies an update archive to the game, it can be the game update or a
|
|
||||||
voicepack update.
|
|
||||||
|
|
||||||
`archive_file` can be a path to the archive file or a file-like object,
|
|
||||||
like if you have very high amount of RAM and want to download the update
|
|
||||||
to memory instead of disk, this can be useful for you.
|
|
||||||
|
|
||||||
`auto_repair` is used to determine whether to repair the file if it's
|
|
||||||
broken. If it's set to False, then it'll raise an exception if the file
|
|
||||||
is broken.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
archive_file (PathLike | IOBase): The archive file.
|
|
||||||
auto_repair (bool, optional): Whether to repair the file if it's broken.
|
|
||||||
Defaults to True.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game is not installed.")
|
|
||||||
if not isinstance(archive_file, IOBase):
|
|
||||||
archive_file = Path(archive_file)
|
|
||||||
# Hello hell again, dealing with HDiffPatch and all the things again.
|
|
||||||
functions.apply_update_archive(self, archive_file, auto_repair=auto_repair)
|
|
||||||
|
|
||||||
def install_update(
|
|
||||||
self, update_info: resource.Patch = None, auto_repair: bool = True
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Installs an update from a `Patch` object.
|
|
||||||
|
|
||||||
You may want to download the update manually and pass it to
|
|
||||||
`apply_update_archive()` instead for better control, and after that
|
|
||||||
execute `set_version_config()` to set the game version.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
update_info (Diff, optional): The update information. Defaults to None.
|
|
||||||
auto_repair (bool, optional): Whether to repair the file if it's broken.
|
|
||||||
Defaults to True.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game is not installed.")
|
|
||||||
if not update_info:
|
|
||||||
update_info = self.get_update()
|
|
||||||
if not update_info or update_info.version == self.get_version_str():
|
|
||||||
raise GameAlreadyUpdatedError("Game is already updated.")
|
|
||||||
update_url = update_info.game_pkgs[0].url
|
|
||||||
# Base game update
|
|
||||||
archive_file = self.cache.joinpath(PurePath(update_url).name)
|
|
||||||
download(update_url, archive_file)
|
|
||||||
self.apply_update_archive(archive_file=archive_file, auto_repair=auto_repair)
|
|
||||||
# Get installed voicepacks
|
|
||||||
installed_voicepacks = self.get_installed_voicepacks()
|
|
||||||
# Voicepack update
|
|
||||||
for remote_voicepack in update_info.audio_pkgs:
|
|
||||||
if remote_voicepack.language not in installed_voicepacks:
|
|
||||||
continue
|
|
||||||
# Voicepack is installed, update it
|
|
||||||
archive_file = self.cache.joinpath(PurePath(remote_voicepack.url).name)
|
|
||||||
download(remote_voicepack.url, archive_file)
|
|
||||||
self.apply_update_archive(
|
|
||||||
archive_file=archive_file, auto_repair=auto_repair
|
|
||||||
)
|
|
||||||
self.set_version_config()
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Re-exports
|
# Re-exports
|
||||||
from vollerei.hsr.patcher import Patcher, PatchType
|
from vollerei.hsr.patcher import Patcher, PatchType
|
||||||
from vollerei.hsr.launcher import Game, GameChannel
|
from vollerei.hsr.launcher import Game
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Patcher", "PatchType", "Game", "GameChannel"]
|
__all__ = ["Patcher", "PatchType", "Game"]
|
||||||
|
|||||||
@ -1,16 +1 @@
|
|||||||
LATEST_VERSION = (2, 5, 0)
|
from vollerei.game.hsr.constants import * # noqa: F403 because we just want to re-export
|
||||||
MD5SUMS = {
|
|
||||||
"1.0.5": {
|
|
||||||
"cn": {
|
|
||||||
"StarRailBase.dll": "66c42871ce82456967d004ccb2d7cf77",
|
|
||||||
"UnityPlayer.dll": "0c866c44bb3752031a8c12ffe935b26f",
|
|
||||||
},
|
|
||||||
"os": {
|
|
||||||
"StarRailBase.dll": "8aa3790aafa3dd176678392f3f93f435",
|
|
||||||
"UnityPlayer.dll": "f17b9b7f9b8c9cbd211bdff7771a80c2",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# Patches
|
|
||||||
ASTRA_REPO = "https://notabug.org/mkrsym1/astra"
|
|
||||||
JADEITE_REPO = "https://codeberg.org/mkrsym1/jadeite/"
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# Re-exports
|
# Re-exports
|
||||||
from vollerei.hsr.launcher.game import Game, GameChannel
|
from vollerei.hsr.launcher.game import Game
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Game", "GameChannel"]
|
__all__ = ["Game"]
|
||||||
|
|||||||
@ -1,24 +1,9 @@
|
|||||||
from configparser import ConfigParser
|
|
||||||
from hashlib import md5
|
|
||||||
from io import IOBase
|
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path, PurePath
|
from vollerei.game.launcher.manager import Game as CommonGame
|
||||||
from vollerei.abc.launcher.game import GameABC
|
from vollerei.common.enums import GameType
|
||||||
from vollerei.common import ConfigFile, functions
|
|
||||||
from vollerei.common.api import resource
|
|
||||||
from vollerei.common.enums import VoicePackLanguage, GameChannel
|
|
||||||
from vollerei.exceptions.game import (
|
|
||||||
GameAlreadyUpdatedError,
|
|
||||||
GameNotInstalledError,
|
|
||||||
PreDownloadNotAvailable,
|
|
||||||
)
|
|
||||||
from vollerei.hsr.constants import MD5SUMS
|
|
||||||
from vollerei.hsr.launcher import api
|
|
||||||
from vollerei import paths
|
|
||||||
from vollerei.utils import download
|
|
||||||
|
|
||||||
|
|
||||||
class Game(GameABC):
|
class Game(CommonGame):
|
||||||
"""
|
"""
|
||||||
Manages the game installation
|
Manages the game installation
|
||||||
|
|
||||||
@ -28,459 +13,4 @@ class Game(GameABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path: PathLike = None, cache_path: PathLike = None):
|
def __init__(self, path: PathLike = None, cache_path: PathLike = None):
|
||||||
self._path: Path | None = Path(path) if path else None
|
super().__init__(GameType.HSR, path, cache_path)
|
||||||
if not cache_path:
|
|
||||||
cache_path = paths.cache_path
|
|
||||||
cache_path = Path(cache_path)
|
|
||||||
self.cache: Path = cache_path.joinpath("game/hsr/")
|
|
||||||
self.cache.mkdir(parents=True, exist_ok=True)
|
|
||||||
self._version_override: tuple[int, int, int] | None = None
|
|
||||||
self._channel_override: GameChannel | None = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version_override(self) -> tuple[int, int, int] | None:
|
|
||||||
"""
|
|
||||||
Overrides the game version.
|
|
||||||
|
|
||||||
This can be useful if you want to override the version of the game
|
|
||||||
and additionally working around bugs.
|
|
||||||
"""
|
|
||||||
return self._version_override
|
|
||||||
|
|
||||||
@version_override.setter
|
|
||||||
def version_override(self, version: tuple[int, int, int] | str | None):
|
|
||||||
if isinstance(version, str):
|
|
||||||
version = tuple(int(i) for i in version.split("."))
|
|
||||||
self._version_override = version
|
|
||||||
|
|
||||||
@property
|
|
||||||
def channel_override(self) -> GameChannel | None:
|
|
||||||
"""
|
|
||||||
Overrides the game channel.
|
|
||||||
|
|
||||||
Because game channel detection isn't implemented yet, you may need
|
|
||||||
to use this for some functions to work.
|
|
||||||
|
|
||||||
This can be useful if you want to override the channel of the game
|
|
||||||
and additionally working around bugs.
|
|
||||||
"""
|
|
||||||
return self._channel_override
|
|
||||||
|
|
||||||
@channel_override.setter
|
|
||||||
def channel_override(self, channel: GameChannel | str | None):
|
|
||||||
if isinstance(channel, str):
|
|
||||||
channel = GameChannel[channel]
|
|
||||||
self._channel_override = channel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self) -> Path | None:
|
|
||||||
"""
|
|
||||||
Paths to the game folder.
|
|
||||||
"""
|
|
||||||
return self._path
|
|
||||||
|
|
||||||
@path.setter
|
|
||||||
def path(self, path: PathLike):
|
|
||||||
self._path = Path(path)
|
|
||||||
|
|
||||||
def data_folder(self) -> Path:
|
|
||||||
"""
|
|
||||||
Paths to the game data folder.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self._path.joinpath("StarRail_Data")
|
|
||||||
except AttributeError:
|
|
||||||
raise GameNotInstalledError("Game path is not set.")
|
|
||||||
|
|
||||||
def is_installed(self) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if the game is installed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the game is installed, False otherwise.
|
|
||||||
"""
|
|
||||||
if self._path is None:
|
|
||||||
return False
|
|
||||||
if (
|
|
||||||
not self._path.joinpath("StarRail.exe").exists()
|
|
||||||
or not self._path.joinpath("StarRail_Data").exists()
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
if self.get_version() == (0, 0, 0):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_channel(self) -> GameChannel:
|
|
||||||
"""
|
|
||||||
Gets the current game channel.
|
|
||||||
|
|
||||||
Only works for Star Rail version 1.0.5, other versions will return the
|
|
||||||
overridden channel or GameChannel.Overseas if no channel is overridden.
|
|
||||||
|
|
||||||
This is not needed for game patching, since the patcher will automatically
|
|
||||||
detect the channel.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
GameChannel: The current game channel.
|
|
||||||
"""
|
|
||||||
version = self._version_override or self.get_version()
|
|
||||||
if 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
|
|
||||||
else:
|
|
||||||
# if self._path.joinpath("StarRail_Data").is_dir():
|
|
||||||
# return GameChannel.Overseas
|
|
||||||
# elif self._path.joinpath("StarRail_Data").exists():
|
|
||||||
# return GameChannel.China
|
|
||||||
# No reliable method there, so we'll just return the overridden channel or
|
|
||||||
# fallback to overseas.
|
|
||||||
return self._channel_override or GameChannel.Overseas
|
|
||||||
|
|
||||||
def get_version_config(self) -> tuple[int, int, int]:
|
|
||||||
"""
|
|
||||||
Gets the current installed game version from config.ini.
|
|
||||||
|
|
||||||
Using this is not recommended, as only official launcher creates
|
|
||||||
and uses this file, instead you should use `get_version()`.
|
|
||||||
|
|
||||||
This returns (0, 0, 0) if the version could not be found.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[int, int, int]: Game version.
|
|
||||||
"""
|
|
||||||
cfg_file = self._path.joinpath("config.ini")
|
|
||||||
if not cfg_file.exists():
|
|
||||||
return (0, 0, 0)
|
|
||||||
cfg = ConfigFile(cfg_file)
|
|
||||||
# Fk u miHoYo
|
|
||||||
if "general" in cfg.sections():
|
|
||||||
version_str = cfg.get("general", "game_version", fallback="0.0.0")
|
|
||||||
elif "General" in cfg.sections():
|
|
||||||
version_str = cfg.get("General", "game_version", fallback="0.0.0")
|
|
||||||
else:
|
|
||||||
return (0, 0, 0)
|
|
||||||
if version_str.count(".") != 2:
|
|
||||||
return (0, 0, 0)
|
|
||||||
try:
|
|
||||||
version = tuple(int(i) for i in version_str.split("."))
|
|
||||||
except Exception:
|
|
||||||
return (0, 0, 0)
|
|
||||||
return version
|
|
||||||
|
|
||||||
def set_version_config(self):
|
|
||||||
"""
|
|
||||||
Sets the current installed game version to config.ini.
|
|
||||||
|
|
||||||
This method is meant to keep compatibility with the official launcher only.
|
|
||||||
"""
|
|
||||||
cfg_file = self._path.joinpath("config.ini")
|
|
||||||
if cfg_file.exists():
|
|
||||||
cfg = ConfigFile(cfg_file)
|
|
||||||
cfg.set("general", "game_version", self.get_version_str())
|
|
||||||
cfg.save()
|
|
||||||
else:
|
|
||||||
cfg = ConfigParser()
|
|
||||||
cfg.read_dict(
|
|
||||||
{
|
|
||||||
"general": {
|
|
||||||
"channel": 1,
|
|
||||||
"cps": "hyp_hoyoverse",
|
|
||||||
"game_version": self.get_version_str(),
|
|
||||||
"sub_channel": 0,
|
|
||||||
# This is probably should be fetched from the server but well
|
|
||||||
"plugin_n06mjyc2r3_version": "1.1.0",
|
|
||||||
"uapc": {
|
|
||||||
"hkrpg_global": {"uapc": "f5c7c6262812_"},
|
|
||||||
"hyp": {"uapc": "f55586a8ce9f_"},
|
|
||||||
}, # Honestly what's this?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cfg.write(cfg_file.open("w"))
|
|
||||||
|
|
||||||
def get_version(self) -> tuple[int, int, int]:
|
|
||||||
"""
|
|
||||||
Gets the current installed game version.
|
|
||||||
|
|
||||||
Credits to An Anime Team for the code that does the magic:
|
|
||||||
https://github.com/an-anime-team/anime-game-core/blob/main/src/games/star_rail/game.rs#L49
|
|
||||||
|
|
||||||
If the above method fails, it'll fallback to read the config.ini file
|
|
||||||
for the version, which is not recommended (as described in
|
|
||||||
`get_version_config()` docs)
|
|
||||||
|
|
||||||
This returns (0, 0, 0) if the version could not be found
|
|
||||||
(usually indicates the game is not installed), and in fact `is_installed()` uses
|
|
||||||
this method to check if the game is installed too.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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 self.get_version_config()
|
|
||||||
|
|
||||||
def bytes_to_int(byte_array: list[bytes]) -> int:
|
|
||||||
bytes_as_int = int.from_bytes(byte_array, byteorder="big")
|
|
||||||
actual_int = bytes_as_int - 48 # 48 is the ASCII code for 0
|
|
||||||
return actual_int
|
|
||||||
|
|
||||||
version_bytes: list[list[bytes]] = [[], [], []]
|
|
||||||
version_ptr = 0
|
|
||||||
correct = True
|
|
||||||
try:
|
|
||||||
with data_file.open("rb") as f:
|
|
||||||
f.seek(0x7D0) # 2000 in decimal
|
|
||||||
for byte in f.read(10000):
|
|
||||||
match byte:
|
|
||||||
case 0:
|
|
||||||
version_bytes = [[], [], []]
|
|
||||||
version_ptr = 0
|
|
||||||
correct = True
|
|
||||||
case 46:
|
|
||||||
version_ptr += 1
|
|
||||||
if version_ptr > 2:
|
|
||||||
correct = False
|
|
||||||
case 38:
|
|
||||||
if (
|
|
||||||
correct
|
|
||||||
and len(version_bytes[0]) > 0
|
|
||||||
and len(version_bytes[1]) > 0
|
|
||||||
and len(version_bytes[2]) > 0
|
|
||||||
):
|
|
||||||
return (
|
|
||||||
bytes_to_int(version_bytes[0]),
|
|
||||||
bytes_to_int(version_bytes[1]),
|
|
||||||
bytes_to_int(version_bytes[2]),
|
|
||||||
)
|
|
||||||
case _:
|
|
||||||
if correct and byte in b"0123456789":
|
|
||||||
version_bytes[version_ptr].append(byte)
|
|
||||||
else:
|
|
||||||
correct = False
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# Fallback to config.ini
|
|
||||||
return self.get_version_config()
|
|
||||||
|
|
||||||
def get_version_str(self) -> str:
|
|
||||||
"""
|
|
||||||
Gets the current installed game version as a string.
|
|
||||||
|
|
||||||
Because this method uses `get_version()`, you should read the docs of
|
|
||||||
that method too.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The version as a string.
|
|
||||||
"""
|
|
||||||
return ".".join(str(i) for i in self.get_version())
|
|
||||||
|
|
||||||
def get_installed_voicepacks(self) -> list[VoicePackLanguage]:
|
|
||||||
"""
|
|
||||||
Gets the installed voicepacks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[VoicePackLanguage]: A list of installed voicepacks.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game is not installed.")
|
|
||||||
voicepacks = []
|
|
||||||
blacklisted_words = ["SFX"]
|
|
||||||
for child in (
|
|
||||||
self.data_folder()
|
|
||||||
.joinpath("Persistent/Audio/AudioPackage/Windows/")
|
|
||||||
.iterdir()
|
|
||||||
):
|
|
||||||
if child.resolve().is_dir() and child.name not in blacklisted_words:
|
|
||||||
try:
|
|
||||||
voicepacks.append(VoicePackLanguage[child.name])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return voicepacks
|
|
||||||
|
|
||||||
def get_remote_game(
|
|
||||||
self, pre_download: bool = False
|
|
||||||
) -> resource.Main | resource.PreDownload:
|
|
||||||
"""
|
|
||||||
Gets the current game information from remote.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A `Main` or `PreDownload` object that contains the game information.
|
|
||||||
"""
|
|
||||||
channel = self._channel_override or self.get_channel()
|
|
||||||
if pre_download:
|
|
||||||
game = api.get_game_package(channel=channel).pre_download
|
|
||||||
if not game:
|
|
||||||
raise PreDownloadNotAvailable("Pre-download version is not available.")
|
|
||||||
return game
|
|
||||||
return api.get_game_package(channel=channel).main
|
|
||||||
|
|
||||||
def get_update(self, pre_download: bool = False) -> resource.Patch | None:
|
|
||||||
"""
|
|
||||||
Gets the current game update.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A `Patch` object that contains the update information or
|
|
||||||
`None` if the game is not installed or already up-to-date.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
return None
|
|
||||||
version = (
|
|
||||||
".".join(str(x) for x in self._version_override)
|
|
||||||
if self._version_override
|
|
||||||
else self.get_version_str()
|
|
||||||
)
|
|
||||||
for patch in self.get_remote_game(pre_download=pre_download).patches:
|
|
||||||
if patch.version == version:
|
|
||||||
return patch
|
|
||||||
return None
|
|
||||||
|
|
||||||
def repair_file(
|
|
||||||
self,
|
|
||||||
file: PathLike,
|
|
||||||
pre_download: bool = False,
|
|
||||||
game_info: resource.Game = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Repairs a game file.
|
|
||||||
|
|
||||||
This will automatically handle backup and restore the file if the repair
|
|
||||||
fails.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file (PathLike): The file to repair.
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
"""
|
|
||||||
return self.repair_files([file], pre_download=pre_download, game_info=game_info)
|
|
||||||
|
|
||||||
def repair_files(
|
|
||||||
self,
|
|
||||||
files: list[PathLike],
|
|
||||||
pre_download: bool = False,
|
|
||||||
game_info: resource.Game = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Repairs multiple game files.
|
|
||||||
|
|
||||||
This will automatically handle backup and restore the file if the repair
|
|
||||||
fails.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
files (PathLike): The files to repair.
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
game_info (resource.Game): The game information to use for repair.
|
|
||||||
"""
|
|
||||||
functions.repair_files(
|
|
||||||
self, files, pre_download=pre_download, game_info=game_info
|
|
||||||
)
|
|
||||||
|
|
||||||
def repair_game(self) -> None:
|
|
||||||
"""
|
|
||||||
Tries to repair the game by reading "pkg_version" file and downloading the
|
|
||||||
mismatched files from the server.
|
|
||||||
"""
|
|
||||||
functions.repair_game(self)
|
|
||||||
|
|
||||||
def install_archive(self, archive_file: PathLike | IOBase) -> None:
|
|
||||||
"""
|
|
||||||
Applies an install archive to the game, it can be the game itself or a
|
|
||||||
voicepack one.
|
|
||||||
|
|
||||||
`archive_file` can be a path to the archive file or a file-like object,
|
|
||||||
like if you have very high amount of RAM and want to download the archive
|
|
||||||
to memory instead of disk, this can be useful for you.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
archive_file (PathLike | IOBase): The archive file.
|
|
||||||
"""
|
|
||||||
if not isinstance(archive_file, IOBase):
|
|
||||||
archive_file = Path(archive_file)
|
|
||||||
functions.install_archive(self, archive_file)
|
|
||||||
|
|
||||||
def apply_update_archive(
|
|
||||||
self, archive_file: PathLike | IOBase, auto_repair: bool = True
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Applies an update archive to the game, it can be the game update or a
|
|
||||||
voicepack update.
|
|
||||||
|
|
||||||
`archive_file` can be a path to the archive file or a file-like object,
|
|
||||||
like if you have very high amount of RAM and want to download the update
|
|
||||||
to memory instead of disk, this can be useful for you.
|
|
||||||
|
|
||||||
`auto_repair` is used to determine whether to repair the file if it's
|
|
||||||
broken. If it's set to False, then it'll raise an exception if the file
|
|
||||||
is broken.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
archive_file (PathLike | IOBase): The archive file.
|
|
||||||
auto_repair (bool, optional): Whether to repair the file if it's broken.
|
|
||||||
Defaults to True.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game is not installed.")
|
|
||||||
if not isinstance(archive_file, IOBase):
|
|
||||||
archive_file = Path(archive_file)
|
|
||||||
# Hello hell again, dealing with HDiffPatch and all the things again.
|
|
||||||
functions.apply_update_archive(self, archive_file, auto_repair=auto_repair)
|
|
||||||
|
|
||||||
def install_update(
|
|
||||||
self, update_info: resource.Patch = None, auto_repair: bool = True
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Installs an update from a `Patch` object.
|
|
||||||
|
|
||||||
You may want to download the update manually and pass it to
|
|
||||||
`apply_update_archive()` instead for better control, and after that
|
|
||||||
execute `set_version_config()` to set the game version.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
update_info (Diff, optional): The update information. Defaults to None.
|
|
||||||
auto_repair (bool, optional): Whether to repair the file if it's broken.
|
|
||||||
Defaults to True.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game is not installed.")
|
|
||||||
if not update_info:
|
|
||||||
update_info = self.get_update()
|
|
||||||
if not update_info or update_info.version == self.get_version_str():
|
|
||||||
raise GameAlreadyUpdatedError("Game is already updated.")
|
|
||||||
update_url = update_info.game_pkgs[0].url
|
|
||||||
# Base game update
|
|
||||||
archive_file = self.cache.joinpath(PurePath(update_url).name)
|
|
||||||
download(update_url, archive_file)
|
|
||||||
self.apply_update_archive(archive_file=archive_file, auto_repair=auto_repair)
|
|
||||||
# Get installed voicepacks
|
|
||||||
installed_voicepacks = self.get_installed_voicepacks()
|
|
||||||
# Voicepack update
|
|
||||||
for remote_voicepack in update_info.audio_pkgs:
|
|
||||||
if remote_voicepack.language not in installed_voicepacks:
|
|
||||||
continue
|
|
||||||
# Voicepack is installed, update it
|
|
||||||
archive_file = self.cache.joinpath(PurePath(remote_voicepack.url).name)
|
|
||||||
download(remote_voicepack.url, archive_file)
|
|
||||||
self.apply_update_archive(
|
|
||||||
archive_file=archive_file, auto_repair=auto_repair
|
|
||||||
)
|
|
||||||
self.set_version_config()
|
|
||||||
|
|||||||
@ -3,13 +3,14 @@ from shutil import copy2, rmtree
|
|||||||
from packaging import version
|
from packaging import version
|
||||||
from vollerei.abc.patcher import PatcherABC
|
from vollerei.abc.patcher import PatcherABC
|
||||||
from vollerei.common import telemetry
|
from vollerei.common import telemetry
|
||||||
|
from vollerei.common.enums import GameChannel
|
||||||
from vollerei.exceptions.game import GameNotInstalledError
|
from vollerei.exceptions.game import GameNotInstalledError
|
||||||
from vollerei.exceptions.patcher import (
|
from vollerei.exceptions.patcher import (
|
||||||
VersionNotSupportedError,
|
VersionNotSupportedError,
|
||||||
PatcherError,
|
PatcherError,
|
||||||
PatchUpdateError,
|
PatchUpdateError,
|
||||||
)
|
)
|
||||||
from vollerei.hsr.launcher.game import Game, GameChannel
|
from vollerei.hsr.launcher.game import Game
|
||||||
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
|
||||||
from vollerei.hsr.constants import ASTRA_REPO, JADEITE_REPO
|
from vollerei.hsr.constants import ASTRA_REPO, JADEITE_REPO
|
||||||
|
|||||||
@ -9,8 +9,8 @@ class Paths:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
base_paths = PlatformDirs("vollerei", "tretrauit", roaming=True)
|
base_paths = PlatformDirs("vollerei", "tretrauit", roaming=True)
|
||||||
cache_path = base_paths.site_cache_path
|
cache_path = base_paths.user_cache_path
|
||||||
data_path = base_paths.site_data_path
|
data_path = base_paths.user_data_path
|
||||||
tools_data_path = data_path.joinpath("tools")
|
tools_data_path = data_path.joinpath("tools")
|
||||||
tools_cache_path = cache_path.joinpath("tools")
|
tools_cache_path = cache_path.joinpath("tools")
|
||||||
launcher_cache_path = cache_path.joinpath("launcher")
|
launcher_cache_path = cache_path.joinpath("launcher")
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from os import PathLike
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@ -86,9 +87,11 @@ class HDiffPatch:
|
|||||||
def hpatchz(self) -> str | None:
|
def hpatchz(self) -> str | None:
|
||||||
return self._get_binary("hpatchz")
|
return self._get_binary("hpatchz")
|
||||||
|
|
||||||
def patch_file(self, in_file, out_file, patch_file):
|
def patch_file(self, in_file: PathLike, out_file: PathLike, patch_file: PathLike):
|
||||||
try:
|
try:
|
||||||
subprocess.check_call([self.hpatchz(), "-f", in_file, patch_file, out_file])
|
subprocess.check_call(
|
||||||
|
[self.hpatchz(), "-f", str(in_file), str(patch_file), str(out_file)]
|
||||||
|
)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
raise HPatchZPatchError("Patch error") from e
|
raise HPatchZPatchError("Patch error") from e
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# Re-exports
|
# Re-exports
|
||||||
from vollerei.zzz.launcher import Game, GameChannel
|
from vollerei.zzz.launcher import Game
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Game", "GameChannel"]
|
__all__ = ["Game"]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# Re-exports
|
# Re-exports
|
||||||
from vollerei.zzz.launcher.game import Game, GameChannel
|
from vollerei.zzz.launcher.game import Game
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Game", "GameChannel"]
|
__all__ = ["Game"]
|
||||||
|
|||||||
@ -1,447 +1,16 @@
|
|||||||
from configparser import ConfigParser
|
|
||||||
from io import IOBase
|
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path, PurePath
|
from vollerei.game.launcher.manager import Game as CommonGame
|
||||||
from vollerei.abc.launcher.game import GameABC
|
from vollerei.common.enums import GameType
|
||||||
from vollerei.common import ConfigFile, functions
|
|
||||||
from vollerei.common.api import resource
|
|
||||||
from vollerei.common.enums import VoicePackLanguage, GameChannel
|
|
||||||
from vollerei.exceptions.game import (
|
|
||||||
GameAlreadyUpdatedError,
|
|
||||||
GameNotInstalledError,
|
|
||||||
PreDownloadNotAvailable,
|
|
||||||
)
|
|
||||||
from vollerei.zzz.launcher import api
|
|
||||||
from vollerei import paths
|
|
||||||
from vollerei.utils import download
|
|
||||||
|
|
||||||
|
|
||||||
class Game(GameABC):
|
class Game(CommonGame):
|
||||||
"""
|
"""
|
||||||
Manages the game installation
|
Manages the game installation
|
||||||
|
|
||||||
Since channel detection isn't (properly) implemented yet, most functions assume you're
|
Since channel detection isn't implemented yet, most functions assume you're
|
||||||
using the overseas version of the game. You can override channel by setting
|
using the overseas version of the game. You can override channel by setting
|
||||||
the property `channel_override` to the channel you want to use.
|
the property `channel_override` to the channel you want to use.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path: PathLike = None, cache_path: PathLike = None):
|
def __init__(self, path: PathLike = None, cache_path: PathLike = None):
|
||||||
self._path: Path | None = Path(path) if path else None
|
super().__init__(GameType.ZZZ, path, cache_path)
|
||||||
if not cache_path:
|
|
||||||
cache_path = paths.cache_path
|
|
||||||
cache_path = Path(cache_path)
|
|
||||||
self.cache: Path = cache_path.joinpath("game/genshin/")
|
|
||||||
self.cache.mkdir(parents=True, exist_ok=True)
|
|
||||||
self._version_override: tuple[int, int, int] | None = None
|
|
||||||
self._channel_override: GameChannel | None = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version_override(self) -> tuple[int, int, int] | None:
|
|
||||||
"""
|
|
||||||
Overrides the game version.
|
|
||||||
|
|
||||||
This can be useful if you want to override the version of the game
|
|
||||||
and additionally working around bugs.
|
|
||||||
"""
|
|
||||||
return self._version_override
|
|
||||||
|
|
||||||
@version_override.setter
|
|
||||||
def version_override(self, version: tuple[int, int, int] | str | None):
|
|
||||||
if isinstance(version, str):
|
|
||||||
version = tuple(int(i) for i in version.split("."))
|
|
||||||
self._version_override = version
|
|
||||||
|
|
||||||
@property
|
|
||||||
def channel_override(self) -> GameChannel | None:
|
|
||||||
"""
|
|
||||||
Overrides the game channel.
|
|
||||||
|
|
||||||
Because game channel detection isn't implemented yet, you may need
|
|
||||||
to use this for some functions to work.
|
|
||||||
|
|
||||||
This can be useful if you want to override the channel of the game
|
|
||||||
and additionally working around bugs.
|
|
||||||
"""
|
|
||||||
return self._channel_override
|
|
||||||
|
|
||||||
@channel_override.setter
|
|
||||||
def channel_override(self, channel: GameChannel | str | None):
|
|
||||||
if isinstance(channel, str):
|
|
||||||
channel = GameChannel[channel]
|
|
||||||
self._channel_override = channel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self) -> Path | None:
|
|
||||||
"""
|
|
||||||
Paths to the game folder.
|
|
||||||
"""
|
|
||||||
return self._path
|
|
||||||
|
|
||||||
@path.setter
|
|
||||||
def path(self, path: PathLike):
|
|
||||||
self._path = Path(path)
|
|
||||||
|
|
||||||
def data_folder(self) -> Path:
|
|
||||||
"""
|
|
||||||
Paths to the game data folder.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self._path.joinpath("ZenlessZoneZero_Data")
|
|
||||||
except AttributeError:
|
|
||||||
raise GameNotInstalledError("Game path is not set.")
|
|
||||||
|
|
||||||
def is_installed(self) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if the game is installed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the game is installed, False otherwise.
|
|
||||||
"""
|
|
||||||
if self._path is None:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
self.get_channel()
|
|
||||||
if self.data_folder().is_dir():
|
|
||||||
return True
|
|
||||||
except GameNotInstalledError:
|
|
||||||
return False
|
|
||||||
if self.get_version() == (0, 0, 0):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_channel(self) -> GameChannel:
|
|
||||||
"""
|
|
||||||
THIS METHOD CURRENTLY DOESN'T WORK YET.
|
|
||||||
|
|
||||||
It'll return the overridden channel if set, otherwise it'll return the
|
|
||||||
overseas channel.
|
|
||||||
|
|
||||||
Gets the current game channel.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
GameChannel: The current game channel.
|
|
||||||
"""
|
|
||||||
return self._channel_override or GameChannel.Overseas
|
|
||||||
|
|
||||||
def get_version_config(self) -> tuple[int, int, int]:
|
|
||||||
"""
|
|
||||||
Gets the current installed game version from config.ini.
|
|
||||||
|
|
||||||
Using this is not recommended, as only official launcher creates
|
|
||||||
and uses this file, instead you should use `get_version()`.
|
|
||||||
|
|
||||||
This returns (0, 0, 0) if the version could not be found.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[int, int, int]: Game version.
|
|
||||||
"""
|
|
||||||
cfg_file = self._path.joinpath("config.ini")
|
|
||||||
if not cfg_file.exists():
|
|
||||||
return (0, 0, 0)
|
|
||||||
cfg = ConfigFile(cfg_file)
|
|
||||||
# Fk u miHoYo
|
|
||||||
if "general" in cfg.sections():
|
|
||||||
version_str = cfg.get("general", "game_version", fallback="0.0.0")
|
|
||||||
elif "General" in cfg.sections():
|
|
||||||
version_str = cfg.get("General", "game_version", fallback="0.0.0")
|
|
||||||
else:
|
|
||||||
return (0, 0, 0)
|
|
||||||
if version_str.count(".") != 2:
|
|
||||||
return (0, 0, 0)
|
|
||||||
try:
|
|
||||||
version = tuple(int(i) for i in version_str.split("."))
|
|
||||||
except Exception:
|
|
||||||
return (0, 0, 0)
|
|
||||||
return version
|
|
||||||
|
|
||||||
def set_version_config(self):
|
|
||||||
"""
|
|
||||||
Sets the current installed game version to config.ini.
|
|
||||||
|
|
||||||
This method is meant to keep compatibility with the official launcher only.
|
|
||||||
"""
|
|
||||||
cfg_file = self._path.joinpath("config.ini")
|
|
||||||
if cfg_file.exists():
|
|
||||||
cfg = ConfigFile(cfg_file)
|
|
||||||
cfg.set("general", "game_version", self.get_version_str())
|
|
||||||
cfg.save()
|
|
||||||
else:
|
|
||||||
cfg = ConfigParser()
|
|
||||||
cfg.read_dict(
|
|
||||||
{
|
|
||||||
"general": {
|
|
||||||
"downloading_mode": None,
|
|
||||||
"channel": 1,
|
|
||||||
"cps": "hyp_hoyoverse",
|
|
||||||
"game_version": self.get_version_str(),
|
|
||||||
"sub_channel": 0,
|
|
||||||
# This is probably should be fetched from the server but well
|
|
||||||
"plugin_cuqph0fsfw_version": "1.0.0",
|
|
||||||
# What's this in the Chinese version?
|
|
||||||
"uapc": {
|
|
||||||
"nap_global": {"uapc": "f55586a8ce9f_"},
|
|
||||||
"hyp": {"uapc": "f55586a8ce9f_"},
|
|
||||||
}, # Honestly what's this?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cfg.write(cfg_file.open("w"))
|
|
||||||
|
|
||||||
def get_version(self) -> tuple[int, int, int]:
|
|
||||||
"""
|
|
||||||
Gets the current installed game version.
|
|
||||||
|
|
||||||
Credits to An Anime Team for the code that does the magic:
|
|
||||||
https://github.com/an-anime-team/anime-game-core/blob/main/src/games/genshin/game.rs#L52
|
|
||||||
|
|
||||||
If the above method fails, it'll fallback to read the config.ini file
|
|
||||||
for the version, which is not recommended (as described in
|
|
||||||
`get_version_config()` docs)
|
|
||||||
|
|
||||||
This returns (0, 0, 0) if the version could not be found
|
|
||||||
(usually indicates the game is not installed), and in fact `is_installed()` uses
|
|
||||||
this method to check if the game is installed too.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[int, int, int]: The version as a tuple of integers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data_file = self.data_folder().joinpath("globalgamemanagers")
|
|
||||||
if not data_file.exists():
|
|
||||||
return self.get_version_config()
|
|
||||||
|
|
||||||
def bytes_to_int(byte_array: list[bytes]) -> int:
|
|
||||||
bytes_as_int = int.from_bytes(byte_array, byteorder="big")
|
|
||||||
actual_int = bytes_as_int - 48 # 48 is the ASCII code for 0
|
|
||||||
return actual_int
|
|
||||||
|
|
||||||
version_bytes: list[list[bytes]] = [[], [], []]
|
|
||||||
version_ptr = 0
|
|
||||||
correct = True
|
|
||||||
try:
|
|
||||||
with data_file.open("rb") as f:
|
|
||||||
f.seek(4000)
|
|
||||||
for byte in f.read(10000):
|
|
||||||
match byte:
|
|
||||||
case 0:
|
|
||||||
version_bytes = [[], [], []]
|
|
||||||
version_ptr = 0
|
|
||||||
correct = True
|
|
||||||
case 46:
|
|
||||||
version_ptr += 1
|
|
||||||
if version_ptr > 2:
|
|
||||||
correct = False
|
|
||||||
case 95:
|
|
||||||
if (
|
|
||||||
correct
|
|
||||||
and len(version_bytes[0]) > 0
|
|
||||||
and len(version_bytes[1]) > 0
|
|
||||||
and len(version_bytes[2]) > 0
|
|
||||||
):
|
|
||||||
return (
|
|
||||||
bytes_to_int(version_bytes[0]),
|
|
||||||
bytes_to_int(version_bytes[1]),
|
|
||||||
bytes_to_int(version_bytes[2]),
|
|
||||||
)
|
|
||||||
case _:
|
|
||||||
if correct and byte in b"0123456789":
|
|
||||||
version_bytes[version_ptr].append(byte)
|
|
||||||
else:
|
|
||||||
correct = False
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# Fallback to config.ini
|
|
||||||
return self.get_version_config()
|
|
||||||
|
|
||||||
def get_version_str(self) -> str:
|
|
||||||
"""
|
|
||||||
Gets the current installed game version as a string.
|
|
||||||
|
|
||||||
Because this method uses `get_version()`, you should read the docs of
|
|
||||||
that method too.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The version as a string.
|
|
||||||
"""
|
|
||||||
return ".".join(str(i) for i in self.get_version())
|
|
||||||
|
|
||||||
def get_installed_voicepacks(self) -> list[VoicePackLanguage]:
|
|
||||||
"""
|
|
||||||
Gets the installed voicepacks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[VoicePackLanguage]: A list of installed voicepacks.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game is not installed.")
|
|
||||||
voicepacks = []
|
|
||||||
for child in (
|
|
||||||
self.data_folder()
|
|
||||||
.joinpath("StreamingAssets/Audio/Windows/Full/")
|
|
||||||
.iterdir()
|
|
||||||
):
|
|
||||||
if child.resolve().is_dir():
|
|
||||||
try:
|
|
||||||
voicepacks.append(VoicePackLanguage.from_zzz_name(child.name))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return voicepacks
|
|
||||||
|
|
||||||
def get_remote_game(
|
|
||||||
self, pre_download: bool = False
|
|
||||||
) -> resource.Main | resource.PreDownload:
|
|
||||||
"""
|
|
||||||
Gets the current game information from remote.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A `Main` or `PreDownload` object that contains the game information.
|
|
||||||
"""
|
|
||||||
channel = self._channel_override or self.get_channel()
|
|
||||||
if pre_download:
|
|
||||||
game = api.get_game_package(channel=channel).pre_download
|
|
||||||
if not game:
|
|
||||||
raise PreDownloadNotAvailable("Pre-download version is not available.")
|
|
||||||
return game
|
|
||||||
return api.get_game_package(channel=channel).main
|
|
||||||
|
|
||||||
def get_update(self, pre_download: bool = False) -> resource.Patch | None:
|
|
||||||
"""
|
|
||||||
Gets the current game update.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A `Patch` object that contains the update information or
|
|
||||||
`None` if the game is not installed or already up-to-date.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
return None
|
|
||||||
version = (
|
|
||||||
".".join(str(x) for x in self._version_override)
|
|
||||||
if self._version_override
|
|
||||||
else self.get_version_str()
|
|
||||||
)
|
|
||||||
for patch in self.get_remote_game(pre_download=pre_download).patches:
|
|
||||||
if patch.version == version:
|
|
||||||
return patch
|
|
||||||
return None
|
|
||||||
|
|
||||||
def repair_file(
|
|
||||||
self,
|
|
||||||
file: PathLike,
|
|
||||||
pre_download: bool = False,
|
|
||||||
game_info: resource.Game = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Repairs a game file.
|
|
||||||
|
|
||||||
This will automatically handle backup and restore the file if the repair
|
|
||||||
fails.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file (PathLike): The file to repair.
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
"""
|
|
||||||
return self.repair_files([file], pre_download=pre_download, game_info=game_info)
|
|
||||||
|
|
||||||
def repair_files(
|
|
||||||
self,
|
|
||||||
files: list[PathLike],
|
|
||||||
pre_download: bool = False,
|
|
||||||
game_info: resource.Game = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Repairs multiple game files.
|
|
||||||
|
|
||||||
This will automatically handle backup and restore the file if the repair
|
|
||||||
fails.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
files (PathLike): The file to repair.
|
|
||||||
pre_download (bool): Whether to get the pre-download version.
|
|
||||||
Defaults to False.
|
|
||||||
"""
|
|
||||||
functions.repair_files(
|
|
||||||
self, files, pre_download=pre_download, game_info=game_info
|
|
||||||
)
|
|
||||||
|
|
||||||
def repair_game(self) -> None:
|
|
||||||
"""
|
|
||||||
Tries to repair the game by reading "pkg_version" file and downloading the
|
|
||||||
mismatched files from the server.
|
|
||||||
"""
|
|
||||||
functions.repair_game(self)
|
|
||||||
|
|
||||||
def apply_update_archive(
|
|
||||||
self, archive_file: PathLike | IOBase, auto_repair: bool = True
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Applies an update archive to the game, it can be the game update or a
|
|
||||||
voicepack update.
|
|
||||||
|
|
||||||
`archive_file` can be a path to the archive file or a file-like object,
|
|
||||||
like if you have very high amount of RAM and want to download the update
|
|
||||||
to memory instead of disk, this can be useful for you.
|
|
||||||
|
|
||||||
`auto_repair` is used to determine whether to repair the file if it's
|
|
||||||
broken. If it's set to False, then it'll raise an exception if the file
|
|
||||||
is broken.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
archive_file (PathLike | IOBase): The archive file.
|
|
||||||
auto_repair (bool, optional): Whether to repair the file if it's broken.
|
|
||||||
Defaults to True.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game is not installed.")
|
|
||||||
if not isinstance(archive_file, IOBase):
|
|
||||||
archive_file = Path(archive_file)
|
|
||||||
# Hello hell again, dealing with HDiffPatch and all the things again.
|
|
||||||
functions.apply_update_archive(self, archive_file, auto_repair=auto_repair)
|
|
||||||
|
|
||||||
def install_update(
|
|
||||||
self, update_info: resource.Patch = None, auto_repair: bool = True
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Installs an update from a `Patch` object.
|
|
||||||
|
|
||||||
You may want to download the update manually and pass it to
|
|
||||||
`apply_update_archive()` instead for better control, and after that
|
|
||||||
execute `set_version_config()` to set the game version.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
update_info (Diff, optional): The update information. Defaults to None.
|
|
||||||
auto_repair (bool, optional): Whether to repair the file if it's broken.
|
|
||||||
Defaults to True.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
|
||||||
raise GameNotInstalledError("Game is not installed.")
|
|
||||||
if not update_info:
|
|
||||||
update_info = self.get_update()
|
|
||||||
if not update_info or update_info.version == self.get_version_str():
|
|
||||||
raise GameAlreadyUpdatedError("Game is already updated.")
|
|
||||||
update_url = update_info.game_pkgs[0].url
|
|
||||||
# Base game update
|
|
||||||
archive_file = self.cache.joinpath(PurePath(update_url).name)
|
|
||||||
download(update_url, archive_file)
|
|
||||||
self.apply_update_archive(archive_file=archive_file, auto_repair=auto_repair)
|
|
||||||
# Get installed voicepacks
|
|
||||||
installed_voicepacks = self.get_installed_voicepacks()
|
|
||||||
# Voicepack update
|
|
||||||
for remote_voicepack in update_info.audio_pkgs:
|
|
||||||
if remote_voicepack.language not in installed_voicepacks:
|
|
||||||
continue
|
|
||||||
# Voicepack is installed, update it
|
|
||||||
archive_file = self.cache.joinpath(PurePath(remote_voicepack.url).name)
|
|
||||||
download(remote_voicepack.url, archive_file)
|
|
||||||
self.apply_update_archive(
|
|
||||||
archive_file=archive_file, auto_repair=auto_repair
|
|
||||||
)
|
|
||||||
self.set_version_config()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user