sssekai 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sssekai might be problematic. Click here for more details.
- sssekai-0.0.1/PKG-INFO +64 -0
- sssekai-0.0.1/README.md +48 -0
- sssekai-0.0.1/setup.cfg +4 -0
- sssekai-0.0.1/setup.py +27 -0
- sssekai-0.0.1/sssekai/__init__.py +6 -0
- sssekai-0.0.1/sssekai/__main__.py +77 -0
- sssekai-0.0.1/sssekai/abcache/__init__.py +188 -0
- sssekai-0.0.1/sssekai/crypto/APIManager.py +24 -0
- sssekai-0.0.1/sssekai/crypto/AssetBundle.py +32 -0
- sssekai-0.0.1/sssekai/crypto/__init__.py +0 -0
- sssekai-0.0.1/sssekai/entrypoint/__init__.py +0 -0
- sssekai-0.0.1/sssekai/entrypoint/abcache.py +15 -0
- sssekai-0.0.1/sssekai/entrypoint/abdecrypt.py +9 -0
- sssekai-0.0.1/sssekai/entrypoint/apidecrypt.py +9 -0
- sssekai-0.0.1/sssekai/entrypoint/live2dextract.py +165 -0
- sssekai-0.0.1/sssekai/entrypoint/mitm.py +7 -0
- sssekai-0.0.1/sssekai/entrypoint/usmdemux.py +32 -0
- sssekai-0.0.1/sssekai/fmt/__init__.py +0 -0
- sssekai-0.0.1/sssekai/fmt/moc3.py +40 -0
- sssekai-0.0.1/sssekai/unity/AnimationClip.py +170 -0
- sssekai-0.0.1/sssekai/unity/AssetBundle.py +13 -0
- sssekai-0.0.1/sssekai/unity/__init__.py +2 -0
- sssekai-0.0.1/sssekai/unity/constant/CommonPathNames.py +1 -0
- sssekai-0.0.1/sssekai/unity/constant/SekaiLive2DPathNames.py +1076 -0
- sssekai-0.0.1/sssekai/unity/constant/__init__.py +0 -0
- sssekai-0.0.1/sssekai.egg-info/PKG-INFO +64 -0
- sssekai-0.0.1/sssekai.egg-info/SOURCES.txt +34 -0
- sssekai-0.0.1/sssekai.egg-info/dependency_links.txt +1 -0
- sssekai-0.0.1/sssekai.egg-info/entry_points.txt +2 -0
- sssekai-0.0.1/sssekai.egg-info/requires.txt +4 -0
- sssekai-0.0.1/sssekai.egg-info/top_level.txt +2 -0
- sssekai-0.0.1/sssekai_blender_io/__init__.py +29 -0
- sssekai-0.0.1/sssekai_blender_io/blender/__init__.py +109 -0
- sssekai-0.0.1/sssekai_blender_io/blender/addon.py +211 -0
- sssekai-0.0.1/sssekai_blender_io/blender/animation.py +123 -0
- sssekai-0.0.1/sssekai_blender_io/blender/asset.py +306 -0
sssekai-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sssekai
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Home-page: https://github.com/mos9527/sssekai
|
|
5
|
+
Author: greats3an
|
|
6
|
+
Author-email: greats3an@gmail.com
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: msgpack
|
|
13
|
+
Requires-Dist: pycryptodome
|
|
14
|
+
Requires-Dist: unitypy
|
|
15
|
+
Requires-Dist: wannacri
|
|
16
|
+
|
|
17
|
+
# sssekai
|
|
18
|
+
Command-line tool for downloading/deobfuscating the game's assets, along with some other tools.
|
|
19
|
+
|
|
20
|
+
usage: sssekai [-h]
|
|
21
|
+
{apidecrypt,abdecrypt,usmdemux,abcache,live2dextract,mitm} ...
|
|
22
|
+
|
|
23
|
+
SSSekai Proejct SEKAI feat. Hatsune Miku (Android) Modding Tools
|
|
24
|
+
Installation:
|
|
25
|
+
pip install git+https://github.com/mos9527/sssekai
|
|
26
|
+
|
|
27
|
+
options:
|
|
28
|
+
-h, --help show this help message and exit
|
|
29
|
+
|
|
30
|
+
subcommands:
|
|
31
|
+
valid subcommands
|
|
32
|
+
|
|
33
|
+
{apidecrypt,abdecrypt,usmdemux,abcache,live2dextract,mitm}
|
|
34
|
+
additional help
|
|
35
|
+
apidecrypt API crypto dumper
|
|
36
|
+
This crypto applies to:
|
|
37
|
+
- API request/response body dumped by packet sniffer (mitmproxy, wireshark, etc.)
|
|
38
|
+
- AssetBundleInfo (can be found at /sdcard/Android/data/com.hermes.mk.asia/files/data/AssetBundleInfo,or see sssekai.abcache)
|
|
39
|
+
abdecrypt Decrypt Sekai AssetBundle
|
|
40
|
+
These can be found at /sdcard/Android/data/com.hermes.mk.asia/files/data/
|
|
41
|
+
usmdemux Demux Sekai USM Video in a AssetBundle
|
|
42
|
+
abcache Sekai AssetBundle local cache
|
|
43
|
+
Downloads/Updates *ALL* PJSK TW assets to local devices.
|
|
44
|
+
NOTE: The assets can take quite a lot of space (est. 27GB) so be prepared
|
|
45
|
+
NOTE: The AssetBundles cached are NOT OBFUSCATED. They can be used as is by various Unity ripping tools (and sssekai by extension)
|
|
46
|
+
that supports stripped Unity version (should be 2020.3.21f1).
|
|
47
|
+
live2dextract Extract Sekai Live2D Models in a AssetBundle
|
|
48
|
+
mitm Run Sekai API MITM proxy (WIP)
|
|
49
|
+
|
|
50
|
+
# sssekai_blender_io
|
|
51
|
+
Blender I/O for the game's assets. (actually it's imports only)
|
|
52
|
+
|
|
53
|
+
Refer to https://github.com/mos9527/sssekai for downloads!
|
|
54
|
+
|
|
55
|
+
Currently supports:
|
|
56
|
+
* Textures
|
|
57
|
+
* Character NPR Material (via [SekaiShaderStandalone](https://github.com/mos9527/sssekai/blob/main/sssekai_blender_io/assets/SekaiShaderStandalone.blend))
|
|
58
|
+
* Static Mesh
|
|
59
|
+
* Skinned Mesh
|
|
60
|
+
* Armatures (built through GameObject hierarchy)
|
|
61
|
+
* Animations (Skeletal, BlendShape)
|
|
62
|
+
|
|
63
|
+
## Installation:
|
|
64
|
+
*WIP*
|
sssekai-0.0.1/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# sssekai
|
|
2
|
+
Command-line tool for downloading/deobfuscating the game's assets, along with some other tools.
|
|
3
|
+
|
|
4
|
+
usage: sssekai [-h]
|
|
5
|
+
{apidecrypt,abdecrypt,usmdemux,abcache,live2dextract,mitm} ...
|
|
6
|
+
|
|
7
|
+
SSSekai Proejct SEKAI feat. Hatsune Miku (Android) Modding Tools
|
|
8
|
+
Installation:
|
|
9
|
+
pip install git+https://github.com/mos9527/sssekai
|
|
10
|
+
|
|
11
|
+
options:
|
|
12
|
+
-h, --help show this help message and exit
|
|
13
|
+
|
|
14
|
+
subcommands:
|
|
15
|
+
valid subcommands
|
|
16
|
+
|
|
17
|
+
{apidecrypt,abdecrypt,usmdemux,abcache,live2dextract,mitm}
|
|
18
|
+
additional help
|
|
19
|
+
apidecrypt API crypto dumper
|
|
20
|
+
This crypto applies to:
|
|
21
|
+
- API request/response body dumped by packet sniffer (mitmproxy, wireshark, etc.)
|
|
22
|
+
- AssetBundleInfo (can be found at /sdcard/Android/data/com.hermes.mk.asia/files/data/AssetBundleInfo,or see sssekai.abcache)
|
|
23
|
+
abdecrypt Decrypt Sekai AssetBundle
|
|
24
|
+
These can be found at /sdcard/Android/data/com.hermes.mk.asia/files/data/
|
|
25
|
+
usmdemux Demux Sekai USM Video in a AssetBundle
|
|
26
|
+
abcache Sekai AssetBundle local cache
|
|
27
|
+
Downloads/Updates *ALL* PJSK TW assets to local devices.
|
|
28
|
+
NOTE: The assets can take quite a lot of space (est. 27GB) so be prepared
|
|
29
|
+
NOTE: The AssetBundles cached are NOT OBFUSCATED. They can be used as is by various Unity ripping tools (and sssekai by extension)
|
|
30
|
+
that supports stripped Unity version (should be 2020.3.21f1).
|
|
31
|
+
live2dextract Extract Sekai Live2D Models in a AssetBundle
|
|
32
|
+
mitm Run Sekai API MITM proxy (WIP)
|
|
33
|
+
|
|
34
|
+
# sssekai_blender_io
|
|
35
|
+
Blender I/O for the game's assets. (actually it's imports only)
|
|
36
|
+
|
|
37
|
+
Refer to https://github.com/mos9527/sssekai for downloads!
|
|
38
|
+
|
|
39
|
+
Currently supports:
|
|
40
|
+
* Textures
|
|
41
|
+
* Character NPR Material (via [SekaiShaderStandalone](https://github.com/mos9527/sssekai/blob/main/sssekai_blender_io/assets/SekaiShaderStandalone.blend))
|
|
42
|
+
* Static Mesh
|
|
43
|
+
* Skinned Mesh
|
|
44
|
+
* Armatures (built through GameObject hierarchy)
|
|
45
|
+
* Animations (Skeletal, BlendShape)
|
|
46
|
+
|
|
47
|
+
## Installation:
|
|
48
|
+
*WIP*
|
sssekai-0.0.1/setup.cfg
ADDED
sssekai-0.0.1/setup.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import sys,os
|
|
2
|
+
|
|
3
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
4
|
+
import setuptools, sssekai
|
|
5
|
+
|
|
6
|
+
with open("README.md", "r", encoding='utf-8') as fh:
|
|
7
|
+
long_description = fh.read()
|
|
8
|
+
|
|
9
|
+
setuptools.setup(
|
|
10
|
+
name="sssekai",
|
|
11
|
+
version=sssekai.__version__,
|
|
12
|
+
author="greats3an",
|
|
13
|
+
author_email="greats3an@gmail.com",
|
|
14
|
+
description="",
|
|
15
|
+
long_description=long_description,
|
|
16
|
+
long_description_content_type="text/markdown",
|
|
17
|
+
url="https://github.com/mos9527/sssekai",
|
|
18
|
+
packages=setuptools.find_packages(),
|
|
19
|
+
classifiers=[
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"License :: OSI Approved :: Apache Software License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
],
|
|
24
|
+
install_requires=["msgpack","pycryptodome","unitypy","wannacri"],
|
|
25
|
+
entry_points={"console_scripts": ["sssekai = sssekai.__main__:__main__"]},
|
|
26
|
+
python_requires=">=3.8",
|
|
27
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import codecs
|
|
2
|
+
import sys, os
|
|
3
|
+
import argparse
|
|
4
|
+
from sssekai.entrypoint.apidecrypt import main_apidecrypt
|
|
5
|
+
from sssekai.entrypoint.abdecrypt import main_abdecrypt
|
|
6
|
+
from sssekai.entrypoint.mitm import main_mitm
|
|
7
|
+
from sssekai.entrypoint.usmdemux import main_usmdemux
|
|
8
|
+
from sssekai.entrypoint.abcache import main_abcache, DEFAULT_CACHE_DIR
|
|
9
|
+
from sssekai.entrypoint.live2dextract import main_live2dextract
|
|
10
|
+
from sssekai.unity import SEKAI_UNITY_VERSION
|
|
11
|
+
def __main__():
|
|
12
|
+
import coloredlogs
|
|
13
|
+
from logging import basicConfig
|
|
14
|
+
coloredlogs.install(
|
|
15
|
+
level='INFO',
|
|
16
|
+
fmt="%(asctime)s %(name)s [%(levelname).4s] %(message)s",
|
|
17
|
+
isatty=True,
|
|
18
|
+
)
|
|
19
|
+
basicConfig(
|
|
20
|
+
level='INFO', format="[%(levelname).4s] %(name)s %(message)s"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
parser = argparse.ArgumentParser(description='''SSSekai Proejct SEKAI feat. Hatsune Miku (Android) Modding Tools
|
|
24
|
+
Installation:
|
|
25
|
+
pip install git+https://github.com/mos9527/sssekai
|
|
26
|
+
''', formatter_class=argparse.RawTextHelpFormatter)
|
|
27
|
+
subparsers = parser.add_subparsers(title='subcommands', description='valid subcommands', help='additional help')
|
|
28
|
+
# apidecrypt
|
|
29
|
+
apidecrypt_parser = subparsers.add_parser('apidecrypt', help='''API crypto dumper
|
|
30
|
+
This crypto applies to:
|
|
31
|
+
- API request/response body dumped by packet sniffer (mitmproxy, wireshark, etc.)
|
|
32
|
+
- AssetBundleInfo (can be found at /sdcard/Android/data/com.hermes.mk.asia/files/data/AssetBundleInfo,or see sssekai.abcache)''')
|
|
33
|
+
apidecrypt_parser.add_argument('infile', type=str, help='input dump file')
|
|
34
|
+
apidecrypt_parser.add_argument('outfile', type=str, help='output json file')
|
|
35
|
+
apidecrypt_parser.set_defaults(func=main_apidecrypt)
|
|
36
|
+
# abdecrypt
|
|
37
|
+
abdecrypt_parser = subparsers.add_parser('abdecrypt', help='''Decrypt Sekai AssetBundle
|
|
38
|
+
These can be found at /sdcard/Android/data/com.hermes.mk.asia/files/data/
|
|
39
|
+
''')
|
|
40
|
+
abdecrypt_parser.add_argument('indir', type=str, help='input directory')
|
|
41
|
+
abdecrypt_parser.add_argument('outdir', type=str, help='output directory')
|
|
42
|
+
abdecrypt_parser.set_defaults(func=main_abdecrypt)
|
|
43
|
+
# usmdemux
|
|
44
|
+
usmdemux_parser = subparsers.add_parser('usmdemux', help='''Demux Sekai USM Video in a AssetBundle''')
|
|
45
|
+
usmdemux_parser.add_argument('infile', type=str, help='input file')
|
|
46
|
+
usmdemux_parser.add_argument('outdir', type=str, help='output directory')
|
|
47
|
+
usmdemux_parser.set_defaults(func=main_usmdemux)
|
|
48
|
+
# abcache
|
|
49
|
+
abcache_parser = subparsers.add_parser('abcache', help='''Sekai AssetBundle local cache
|
|
50
|
+
Downloads/Updates *ALL* PJSK TW assets to local devices.
|
|
51
|
+
NOTE: The assets can take quite a lot of space (est. 27GB) so be prepared
|
|
52
|
+
NOTE: The AssetBundles cached are NOT OBFUSCATED. They can be used as is by various Unity ripping tools (and sssekai by extension)
|
|
53
|
+
that supports stripped Unity version (should be %s).''' % SEKAI_UNITY_VERSION)
|
|
54
|
+
abcache_parser.add_argument('--cache-dir', type=str, help='cache directory',default=DEFAULT_CACHE_DIR)
|
|
55
|
+
abcache_parser.add_argument('--skip-update',action='store_true',help='skip all updates and use cached assets as is.')
|
|
56
|
+
abcache_parser.add_argument('--open', action='store_true',help='open cache directory. this will skip all updates.')
|
|
57
|
+
abcache_parser.set_defaults(func=main_abcache)
|
|
58
|
+
# live2dextract
|
|
59
|
+
live2dextract_parser = subparsers.add_parser('live2dextract', help='''Extract Sekai Live2D Models in a AssetBundle''')
|
|
60
|
+
live2dextract_parser.add_argument('infile', type=str, help='input file')
|
|
61
|
+
live2dextract_parser.add_argument('outdir', type=str, help='output directory')
|
|
62
|
+
live2dextract_parser.add_argument('--no-anim',action='store_true',help='don\'t extract animation clips')
|
|
63
|
+
live2dextract_parser.set_defaults(func=main_live2dextract)
|
|
64
|
+
# mitm
|
|
65
|
+
mitm_parser = subparsers.add_parser('mitm', help='Run Sekai API MITM proxy (WIP)')
|
|
66
|
+
mitm_parser.set_defaults(func=main_mitm)
|
|
67
|
+
# parse args
|
|
68
|
+
args = parser.parse_args()
|
|
69
|
+
if 'func' in args:
|
|
70
|
+
args.func(args)
|
|
71
|
+
else:
|
|
72
|
+
parser.print_help()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
__main__()
|
|
77
|
+
sys.exit(0)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from os import path, remove, makedirs, stat
|
|
2
|
+
from typing import List, Mapping
|
|
3
|
+
from dataclasses import dataclass, asdict
|
|
4
|
+
from requests import get as http_get, Session
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from json import dump, load
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
logger = getLogger('sssekai.abcache')
|
|
10
|
+
|
|
11
|
+
from sssekai.crypto.APIManager import decrypt
|
|
12
|
+
from sssekai.crypto.AssetBundle import decrypt_headaer_inplace, SEKAI_AB_MAGIC
|
|
13
|
+
from msgpack import unpackb
|
|
14
|
+
from tqdm import tqdm
|
|
15
|
+
DEFAULT_CACHE_DIR = '~/.sssekai/abcache'
|
|
16
|
+
# 2.6.1
|
|
17
|
+
SEKAI_CDN = 'https://lf16-mkovscdn-sg.bytedgame.com/obj/sf-game-alisg/gdl_app_5245/'
|
|
18
|
+
SEKAI_AB_BASE_PATH = 'AssetBundle/2.6.0/Release/online/'
|
|
19
|
+
SEKAI_AB_INDEX_PATH = SEKAI_AB_BASE_PATH + 'android21/AssetBundleInfo.json'
|
|
20
|
+
|
|
21
|
+
class ThreadpoolDownloader(ThreadPoolExecutor):
|
|
22
|
+
session : Session
|
|
23
|
+
progress : tqdm
|
|
24
|
+
|
|
25
|
+
def download(self, url, fname, length):
|
|
26
|
+
RETRIES = 1
|
|
27
|
+
for _ in range(0,RETRIES):
|
|
28
|
+
try:
|
|
29
|
+
resp = self.session.get(url,stream=True)
|
|
30
|
+
resp.raise_for_status()
|
|
31
|
+
makedirs(path.dirname(fname),exist_ok=True)
|
|
32
|
+
with open(fname, 'wb') as f:
|
|
33
|
+
magic = next(resp.iter_content(4))
|
|
34
|
+
if magic == SEKAI_AB_MAGIC:
|
|
35
|
+
header = next(resp.iter_content(128))
|
|
36
|
+
self.progress.update(128)
|
|
37
|
+
f.write(decrypt_headaer_inplace(bytearray(header)))
|
|
38
|
+
else:
|
|
39
|
+
f.write(magic)
|
|
40
|
+
for chunk in resp.iter_content(65536):
|
|
41
|
+
self.progress.update(len(chunk))
|
|
42
|
+
f.write(chunk)
|
|
43
|
+
return
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.error('While downloading %s : %s. Retrying' % (url,e))
|
|
46
|
+
if _ == RETRIES - 1:
|
|
47
|
+
logger.critical('Did not download %s' % url)
|
|
48
|
+
def add_link(self, url, fname, length):
|
|
49
|
+
self.progress.total += length
|
|
50
|
+
return self.submit(self.download, url, fname, length)
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.session = Session()
|
|
53
|
+
self.progress = tqdm(bar_format="{desc}: {percentage:.1f}%|{bar}| {n_fmt}/{total_fmt} {rate_fmt} {elapsed}<{remaining}", total=0,unit='B', unit_scale=True, unit_divisor=1024,desc="Downloading")
|
|
54
|
+
super().__init__() # workers = processor count * 5
|
|
55
|
+
|
|
56
|
+
class AbCacheConfig:
|
|
57
|
+
cache_dir : str # absolute path to cache directory
|
|
58
|
+
downloader : ThreadpoolDownloader
|
|
59
|
+
def __init__(self, downloader : ThreadpoolDownloader, cache_dir: str = DEFAULT_CACHE_DIR) -> None:
|
|
60
|
+
self.cache_dir = path.expanduser(cache_dir)
|
|
61
|
+
self.cache_dir = path.abspath(self.cache_dir)
|
|
62
|
+
self.downloader = downloader
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class AbCacheEntry(dict):
|
|
66
|
+
bundleName: str
|
|
67
|
+
cacheFileName: str
|
|
68
|
+
cacheDirectoryName: str
|
|
69
|
+
hash: str
|
|
70
|
+
category: str
|
|
71
|
+
crc: int
|
|
72
|
+
fileSize:int
|
|
73
|
+
dependencies : List[str]
|
|
74
|
+
paths : List[str]
|
|
75
|
+
isBuiltin : bool
|
|
76
|
+
md5Hash : str
|
|
77
|
+
downloadPath : str
|
|
78
|
+
|
|
79
|
+
def up_to_date(self, config: AbCacheConfig, other) -> bool:
|
|
80
|
+
fsize = self.get_file_size(config)
|
|
81
|
+
return self.hash == other.hash and self.fileSize <= fsize # fsize may be fileSize + 4
|
|
82
|
+
|
|
83
|
+
def get_file_size(self, config: AbCacheConfig):
|
|
84
|
+
if self.get_file_exists(config):
|
|
85
|
+
return stat(self.get_file_path(config)).st_size
|
|
86
|
+
else:
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
def get_file_exists(self, config: AbCacheConfig) -> bool:
|
|
90
|
+
return path.exists(self.get_file_path(config))
|
|
91
|
+
|
|
92
|
+
def get_file_path(self, config: AbCacheConfig) -> str:
|
|
93
|
+
return path.join(config.cache_dir, self.bundleName)
|
|
94
|
+
@dataclass
|
|
95
|
+
class AbCacheIndex(dict):
|
|
96
|
+
version : str
|
|
97
|
+
bundles : Mapping[str, AbCacheEntry]
|
|
98
|
+
def get_bundle_by_abcache_path(self, config: AbCacheConfig, fpath) -> AbCacheEntry | None:
|
|
99
|
+
# "C:\Users\mos9527\.sssekai\abcache\title_screen\anniversary_2nd_bg"
|
|
100
|
+
fpath = Path(fpath)
|
|
101
|
+
cpath = Path(config.cache_dir)
|
|
102
|
+
assert fpath.is_relative_to(cpath), "File provided is not inside the cache"
|
|
103
|
+
relpath = fpath.relative_to(cpath).as_posix()
|
|
104
|
+
return self.bundles.get(relpath,None)
|
|
105
|
+
|
|
106
|
+
class AbCache:
|
|
107
|
+
config : AbCacheConfig
|
|
108
|
+
index : AbCacheIndex
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def download_cache_index() -> AbCacheIndex:
|
|
112
|
+
resp = http_get(
|
|
113
|
+
url=SEKAI_CDN + SEKAI_AB_INDEX_PATH,
|
|
114
|
+
headers={
|
|
115
|
+
'Accept-Encoding': 'deflate, gzip'
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
resp.raise_for_status()
|
|
119
|
+
data = decrypt(resp.content)
|
|
120
|
+
data = unpackb(data)
|
|
121
|
+
cache = AbCacheIndex(**data)
|
|
122
|
+
for k,v in cache.bundles.items():
|
|
123
|
+
cache.bundles[k] = AbCacheEntry(**v)
|
|
124
|
+
return cache
|
|
125
|
+
|
|
126
|
+
def update_cahce_entry(self, entry : AbCacheEntry, new_entry : AbCacheEntry = None):
|
|
127
|
+
if new_entry:
|
|
128
|
+
entry = new_entry
|
|
129
|
+
return self.config.downloader.add_link(
|
|
130
|
+
SEKAI_CDN + SEKAI_AB_BASE_PATH + entry.downloadPath,
|
|
131
|
+
entry.get_file_path(self.config),
|
|
132
|
+
entry.fileSize
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def update_cahce_index(self):
|
|
136
|
+
logger.info('Downloading AssetBundleInfo.json')
|
|
137
|
+
dl = AbCache.download_cache_index()
|
|
138
|
+
all_keys = sorted(list({k for k in dl.bundles.keys()}.union({k for k in self.index.bundles.keys()})))
|
|
139
|
+
update_count = 0
|
|
140
|
+
for k in all_keys:
|
|
141
|
+
if k in dl.bundles and k in self.index.bundles:
|
|
142
|
+
# update
|
|
143
|
+
if not dl.bundles[k].up_to_date(self.config, self.index.bundles[k]):
|
|
144
|
+
logger.info('Updating bundle %s', k)
|
|
145
|
+
self.update_cahce_entry(self.index.bundles[k], dl.bundles[k])
|
|
146
|
+
update_count+=1
|
|
147
|
+
else:
|
|
148
|
+
logger.debug('Bundle %s is up to date' % k)
|
|
149
|
+
pass
|
|
150
|
+
elif k in dl.bundles:
|
|
151
|
+
# append
|
|
152
|
+
logger.info('Adding bundle %s', k)
|
|
153
|
+
self.index.bundles[k] = dl.bundles[k]
|
|
154
|
+
self.update_cahce_entry(self.index.bundles[k])
|
|
155
|
+
update_count+=1
|
|
156
|
+
else:
|
|
157
|
+
# removal
|
|
158
|
+
logger.info('Removing bundle %s', k)
|
|
159
|
+
if path.exists(self.index.bundles[k].get_file_path(self.config)):
|
|
160
|
+
remove(self.index.bundles[k].get_file_path(self.config))
|
|
161
|
+
del self.index.bundles[k]
|
|
162
|
+
logger.info('Saving AssetBundle index')
|
|
163
|
+
self.save()
|
|
164
|
+
logger.info('...Saved. Need %d updates' % update_count)
|
|
165
|
+
self.config.downloader.shutdown(wait=True)
|
|
166
|
+
logger.info('AssetBundles are now up-to-date')
|
|
167
|
+
|
|
168
|
+
def __init__(self, config : AbCacheConfig) -> None:
|
|
169
|
+
self.config = config
|
|
170
|
+
makedirs(self.config.cache_dir,exist_ok=True)
|
|
171
|
+
try:
|
|
172
|
+
self.load()
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.warning('Failed to load cache index. Creating new one. (%s)' % e)
|
|
175
|
+
self.index = AbCacheIndex(None, {})
|
|
176
|
+
self.save()
|
|
177
|
+
|
|
178
|
+
def save(self):
|
|
179
|
+
with open(path.join(self.config.cache_dir, 'abindex.json'),'w',encoding='utf-8') as f:
|
|
180
|
+
dump(asdict(self.index), f, indent=4, ensure_ascii=False)
|
|
181
|
+
logger.info("Saved %d entries to cache index" % len(self.index.bundles))
|
|
182
|
+
|
|
183
|
+
def load(self):
|
|
184
|
+
with open(path.join(self.config.cache_dir, 'abindex.json'),'r',encoding='utf-8') as f:
|
|
185
|
+
self.index = AbCacheIndex(**load(f))
|
|
186
|
+
for k,v in self.index.bundles.items():
|
|
187
|
+
self.index.bundles[k] = AbCacheEntry(**v)
|
|
188
|
+
logger.info("Loaded %d entries from cache index" % len(self.index.bundles))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from Crypto.Cipher import AES
|
|
2
|
+
def PKCS7_pad(data : bytes,bs) -> bytes:
|
|
3
|
+
size = (bs - len(data) % bs)
|
|
4
|
+
return data + bytes([size] * size)
|
|
5
|
+
|
|
6
|
+
def PKCS7_unpad(data : bytes,bs) -> bytes:
|
|
7
|
+
pad = data[-1]
|
|
8
|
+
return data[:-pad]
|
|
9
|
+
|
|
10
|
+
def decrypt_aes_cbc(data : bytes, key, iv, unpad=PKCS7_unpad) -> bytes:
|
|
11
|
+
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
12
|
+
return unpad(cipher.decrypt(data), cipher.block_size)
|
|
13
|
+
|
|
14
|
+
def encrypt_aes_cbc(data : bytes, key, iv, pad=PKCS7_pad) -> bytes:
|
|
15
|
+
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
16
|
+
return cipher.encrypt(pad(data, cipher.block_size))
|
|
17
|
+
|
|
18
|
+
SEKAI_APIMANAGER_KEY = b"g2fcC0ZczN9MTJ61"
|
|
19
|
+
SEKAI_APIMANAGER_IV = b"msx3IV0i9XE5uYZ1"
|
|
20
|
+
def encrypt(data : bytes) -> bytes:
|
|
21
|
+
return encrypt_aes_cbc(data,SEKAI_APIMANAGER_KEY,SEKAI_APIMANAGER_IV, PKCS7_pad)
|
|
22
|
+
|
|
23
|
+
def decrypt(data : bytes) -> bytes:
|
|
24
|
+
return decrypt_aes_cbc(data,SEKAI_APIMANAGER_KEY,SEKAI_APIMANAGER_IV, PKCS7_unpad)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
from typing import BinaryIO
|
|
3
|
+
|
|
4
|
+
SEKAI_AB_MAGIC = b'\x10\x00\x00\x00'
|
|
5
|
+
def decrypt_headaer_inplace(header : bytearray):
|
|
6
|
+
assert len(header) == 128
|
|
7
|
+
for i in range(0,128,8):
|
|
8
|
+
for j in range(5):
|
|
9
|
+
header[i + j] = ~header[i + j] & 0xff
|
|
10
|
+
return header
|
|
11
|
+
|
|
12
|
+
def decrypt(fin : BinaryIO, fout : BinaryIO = None) -> None:
|
|
13
|
+
if not fout:
|
|
14
|
+
fout = BytesIO()
|
|
15
|
+
magic = fin.read(4)
|
|
16
|
+
if magic == SEKAI_AB_MAGIC:
|
|
17
|
+
fout.write(decrypt_headaer_inplace(bytearray(fin.read(128))))
|
|
18
|
+
while (block := fin.read(65536)):
|
|
19
|
+
fout.write(block)
|
|
20
|
+
else:
|
|
21
|
+
fin.seek(0)
|
|
22
|
+
while (block := fin.read(65536)):
|
|
23
|
+
fout.write(block)
|
|
24
|
+
return fout
|
|
25
|
+
|
|
26
|
+
def encrypt(fin, fout):
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
def has_magic(fin : BinaryIO) -> bool:
|
|
30
|
+
magic = fin.read(4)
|
|
31
|
+
fin.seek(0)
|
|
32
|
+
return magic == SEKAI_AB_MAGIC
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from sssekai.abcache import AbCache, AbCacheConfig,ThreadpoolDownloader, logger, DEFAULT_CACHE_DIR
|
|
2
|
+
def main_abcache(args):
|
|
3
|
+
downloader = ThreadpoolDownloader()
|
|
4
|
+
cache_dir = args.cache_dir or DEFAULT_CACHE_DIR
|
|
5
|
+
config = AbCacheConfig(downloader, cache_dir)
|
|
6
|
+
if args.open:
|
|
7
|
+
from os import startfile
|
|
8
|
+
startfile(config.cache_dir)
|
|
9
|
+
return
|
|
10
|
+
cache = AbCache(config)
|
|
11
|
+
if not args.skip_update:
|
|
12
|
+
cache.update_cahce_index()
|
|
13
|
+
downloader.shutdown()
|
|
14
|
+
cache.save()
|
|
15
|
+
logger.info('AssetBundle cache is now ready. Visit: %s' % cache_dir)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os
|
|
2
|
+
def main_abdecrypt(args):
|
|
3
|
+
from sssekai.crypto.AssetBundle import decrypt
|
|
4
|
+
tree = os.walk(args.indir)
|
|
5
|
+
for root, dirs, files in tree:
|
|
6
|
+
for fname in files:
|
|
7
|
+
file = os.path.join(root,fname)
|
|
8
|
+
if (os.path.isfile(file)):
|
|
9
|
+
decrypt(open(file,'rb'), open(os.path.join(args.outdir, fname),'wb'))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os
|
|
2
|
+
def main_apidecrypt(args):
|
|
3
|
+
from sssekai.crypto.APIManager import decrypt
|
|
4
|
+
import json, msgpack
|
|
5
|
+
data = open(args.infile,'rb').read()
|
|
6
|
+
with open(args.outfile,'w',encoding='utf-8') as fout:
|
|
7
|
+
plain = decrypt(data)
|
|
8
|
+
msg = msgpack.unpackb(plain)
|
|
9
|
+
json.dump(msg, fout, indent=4, ensure_ascii=False)
|