diff --git a/README.md b/README.md index cec971e..a441d9f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # sssekai -Command-line tool (w/Python API support) for downloading/deobfuscating the game's assets, along with some other tools. +Command-line tool (w/Python API support) for Project SEKAI (JP: プロジェクトセカイ カラフルステージ! feat.初音ミク) game assets. # Installation **For Windows Users** : Builds are available [here](https://github.com/mos9527/sssekai/releases) diff --git a/sssekai/__init__.py b/sssekai/__init__.py index 0450132..bbbf972 100644 --- a/sssekai/__init__.py +++ b/sssekai/__init__.py @@ -1,5 +1,5 @@ __VERSION_MAJOR__ = 0 -__VERSION_MINOR__ = 2 -__VERSION_PATCH__ = 9 +__VERSION_MINOR__ = 3 +__VERSION_PATCH__ = 0 __version__ = '%s.%s.%s' % (__VERSION_MAJOR__,__VERSION_MINOR__,__VERSION_PATCH__) diff --git a/sssekai/__main__.py b/sssekai/__main__.py index 0620709..0c23e8f 100644 --- a/sssekai/__main__.py +++ b/sssekai/__main__.py @@ -8,6 +8,9 @@ from sssekai.entrypoint.abcache import main_abcache, DEFAULT_CACHE_DB_FILE from sssekai.entrypoint.live2dextract import main_live2dextract from sssekai.entrypoint.spineextract import main_spineextract +from sssekai.entrypoint.apphash import main_apphash +from sssekai.entrypoint.mvdata import main_mvdata +from sssekai.entrypoint.moc3paths import main_moc3paths from sssekai.unity import sssekai_get_unity_version,sssekai_set_unity_version def __main__(): from tqdm.std import tqdm as tqdm_c @@ -82,6 +85,20 @@ def write(__s): rla2json_parser.add_argument('infile', type=str, help='input file') rla2json_parser.add_argument('outdir', type=str, help='output directory. multiple json files may be produced') rla2json_parser.set_defaults(func=main_rla2json) + # apphash + apphash_parser = subparsers.add_parser('apphash', help='''Download/extract game AppHash values''') + apphash_parser.add_argument('--apk-src', type=str, help='APK source file (default: fetch from APKPure)', default=None) + apphash_parser.add_argument('--fetch', action='store_true', help='force fetching the latest APK') + apphash_parser.set_defaults(func=main_apphash) + # mvdata + mvdata_parser = subparsers.add_parser('mvdata', help='''Extract MV Data from AssetBundle''') + mvdata_parser.add_argument('cache_dir', type=str, help='cache directory') + mvdata_parser.add_argument('query', type=str, help='MV ID') + mvdata_parser.set_defaults(func=main_mvdata) + # moc3paths + moc3paths_parser = subparsers.add_parser('moc3paths', help='''Extract animation path CRCs from raw .moc3 binaries''') + moc3paths_parser.add_argument('indir', type=str, help='input directory') + moc3paths_parser.set_defaults(func=main_moc3paths) # parse args args = parser.parse_args() # set logging level diff --git a/sssekai/scripts/dump_android_pjsk_appHash.py b/sssekai/entrypoint/apphash.py similarity index 84% rename from sssekai/scripts/dump_android_pjsk_appHash.py rename to sssekai/entrypoint/apphash.py index d4f09e0..6c4990f 100644 --- a/sssekai/scripts/dump_android_pjsk_appHash.py +++ b/sssekai/entrypoint/apphash.py @@ -38,19 +38,15 @@ def enum_package(zip_file): if f.filename.lower().endswith('.apk'): yield zipfile.ZipFile(zip_file.open(f)) -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Dump appHash from Android APK') - parser.add_argument('--apk-src','-s', help='APK/APKX file to dump from') - parser.add_argument('--fetch', '-f', help='Use the latest game package from APKPure instead. Recommended for most cases.', action='store_true') - args = parser.parse_args() +def main_apphash(args): env = UnityPy.Environment() - if args.fetch: + if not args.apk_src or args.fetch: from requests import get src = BytesIO() - print('Fetching latest game package from APKPure.') + print('Fetching latest game package from APKPure') resp = get('https://d.apkpure.net/b/XAPK/com.sega.pjsekai?version=latest', stream=True) size = resp.headers.get('Content-Length',-1) - for chunck in resp.iter_content(chunk_size=2**20): + for chunck in resp.iter_content(chunk_size=2**10): src.write(chunck) print('Downloading %d/%s' % (src.tell(), size), end='\r') print() diff --git a/sssekai/entrypoint/moc3paths.py b/sssekai/entrypoint/moc3paths.py new file mode 100644 index 0000000..2295db4 --- /dev/null +++ b/sssekai/entrypoint/moc3paths.py @@ -0,0 +1,32 @@ +from io import BytesIO +from sssekai.unity.AssetBundle import load_assetbundle +from sssekai.fmt.moc3 import read_moc3 +import sys, os +from UnityPy.enums import ClassIDType + +def main_moc3paths(args): + ParameterNames = set() + PartNames = set() + tree = os.walk(args.indir) + for root, dirs, files in tree: + for fname in files: + file = os.path.join(root,fname) + with open(file,'rb') as f: + env = load_assetbundle(f) + for obj in env.objects: + if obj.type == ClassIDType.TextAsset: + data = obj.read() + out_name : str = data.name + if out_name.endswith('.moc3'): + parts, parameters = read_moc3(BytesIO(data.script.tobytes())) + ParameterNames.update(parameters) + PartNames.update(parts) + from zlib import crc32 + print('NAMES_CRC_TBL = {') + for name in sorted(list(PartNames)): + fullpath = 'Parts/' + name + print(' %d:"%s",' % (crc32(fullpath.encode('utf-8')), fullpath)) + for name in sorted(list(ParameterNames)): + fullpath = 'Parameters/' + name + print(' %d:"%s",' % (crc32(fullpath.encode('utf-8')), fullpath)) + print('}') \ No newline at end of file diff --git a/sssekai/scripts/dump_mvdata.py b/sssekai/entrypoint/mvdata.py similarity index 73% rename from sssekai/scripts/dump_mvdata.py rename to sssekai/entrypoint/mvdata.py index 6c85ef5..438ac01 100644 --- a/sssekai/scripts/dump_mvdata.py +++ b/sssekai/entrypoint/mvdata.py @@ -1,6 +1,6 @@ from sssekai.unity.AssetBundle import load_assetbundle -from sssekai.abcache import AbCache, AbCacheConfig import os, json + def main_mvdata(args): from UnityPy.enums import ClassIDType cache_dir = args.cache_dir @@ -24,11 +24,3 @@ def main_mvdata(args): break mvdata = mvdata_items[mvdata_lut[args.query]] print(json.dumps(mvdata, indent=4,ensure_ascii=False)) - -if __name__ == '__main__': - import argparse - parser = argparse.ArgumentParser(description='Dump MVData') - parser.add_argument('cache_dir', help='live_pv\mv_data directory. Downloaded by sssekai abcache.') - parser.add_argument('query', help='Query') - args = parser.parse_args() - main_mvdata(args) \ No newline at end of file diff --git a/sssekai/scripts/dump_moc3_animation_path.py b/sssekai/scripts/dump_moc3_animation_path.py deleted file mode 100644 index 85fc3b9..0000000 --- a/sssekai/scripts/dump_moc3_animation_path.py +++ /dev/null @@ -1,31 +0,0 @@ -from io import BytesIO -from sssekai.unity.AssetBundle import load_assetbundle -from sssekai.fmt.moc3 import read_moc3 -import sys, os -from UnityPy.enums import ClassIDType - -ParameterNames = set() -PartNames = set() -tree = os.walk(sys.argv[1]) -for root, dirs, files in tree: - for fname in files: - file = os.path.join(root,fname) - with open(file,'rb') as f: - env = load_assetbundle(f) - for obj in env.objects: - if obj.type == ClassIDType.TextAsset: - data = obj.read() - out_name : str = data.name - if out_name.endswith('.moc3'): - parts, parameters = read_moc3(BytesIO(data.script.tobytes())) - ParameterNames.update(parameters) - PartNames.update(parts) -from zlib import crc32 -print('NAMES_CRC_TBL = {') -for name in sorted(list(PartNames)): - fullpath = 'Parts/' + name - print(' %d:"%s",' % (crc32(fullpath.encode('utf-8')), fullpath)) -for name in sorted(list(ParameterNames)): - fullpath = 'Parameters/' + name - print(' %d:"%s",' % (crc32(fullpath.encode('utf-8')), fullpath)) -print('}') \ No newline at end of file diff --git a/sssekai/scripts/dump_source_anim_paths.py b/sssekai/scripts/dump_source_anim_paths.py deleted file mode 100644 index 053b01a..0000000 --- a/sssekai/scripts/dump_source_anim_paths.py +++ /dev/null @@ -1,14 +0,0 @@ -import sys -paths = set() -with open(sys.argv[1],'r',encoding='utf-8') as f: - while line := f.readline(): - line = line.strip() - if 'path:' in line or 'attribute:' in line: - path = line.split(':')[-1] - paths.add(path) - -from zlib import crc32 -print('NAMES_CRC_TBL = {') -for path in sorted(list(paths)): - print(' %d:"%s",' % (crc32(path.encode('utf-8')), path)) -print('}') \ No newline at end of file diff --git a/sssekai/scripts/mitmproxy_sekai_api.py b/sssekai/scripts/mitmproxy_sekai_api.py deleted file mode 100644 index 6122999..0000000 --- a/sssekai/scripts/mitmproxy_sekai_api.py +++ /dev/null @@ -1,77 +0,0 @@ -import json -from mitmproxy import http -from crypto.APIManager import encrypt, decrypt -from msgpack import unpackb, packb -import datetime, copy - -class APIIntereceptor: - @staticmethod - def file_id(flow: http.HTTPFlow): - return '%s_%s' % (datetime.datetime.now().strftime("%H_%M_%S") , flow.request.path.split('/')[-1].split('.')[0].split('?')[0]) - - @staticmethod - def filter(flow: http.HTTPFlow): - TW_HOSTS = { - 'mk-zian-obt-cdn.bytedgame.com', - 'mk-zian-obt-https.bytedgame.com' - } - return flow.request.host_header in TW_HOSTS - - @staticmethod - def log_flow(data : bytes, flow: http.HTTPFlow) -> dict | None: - if (len(data)): - prefix = 'request' - if (flow.response): - prefix = 'response' - try: - print(">>> %s <<<" % prefix.upper()) - decrypted_body = decrypt(data) - body = unpackb(decrypted_body) - print("::",flow.request.url) - with open('logs/%s_%s.json' % (prefix,APIIntereceptor.file_id(flow)),'w',encoding='utf-8') as f: - f.write(json.dumps(body,indent=4,ensure_ascii=False)) - return body - except Exception as e: - print(">>> ERROR <<<") - print(e) - print(flow.request.url) - with open('logs_bin/%s_%s.bin' % (prefix,APIIntereceptor.file_id(flow)),'wb') as f: - if (flow.response): - f.write(flow.response.content) - else: - f.write(flow.request.content) - - def request(self,flow: http.HTTPFlow): - print(flow.request.host_header) - if self.filter(flow): - body = self.log_flow(flow.request.content, flow) - if body: - if 'musicDifficultyId' in body: - print('! Intercepted Live request') - body['musicId'] = 1 # Tell Your World - body['musicDifficultyId'] = 4 # Expert - flow.request.content = encrypt(packb(body)) - - def response(self, flow : http.HTTPFlow): - if self.filter(flow): - body = self.log_flow(flow.response.content, flow) - if body: - if 'userMusics' in body: - print('! Intercepted userMusics') - existing = {music['musicId'] : music for music in body['userMusics']} - body['userMusics'] = [] - for i in range(1, 1000): - if i in existing: - body['userMusics'].append(existing[i]) - else: - print('Appended %d' % i) - new_song = copy.deepcopy(existing[1]) - new_song['musicId'] = i - - for stat in body['userMusics'][-1]['userMusicDifficultyStatuses']: - stat['musicDifficultyStatus'] = 'available' - flow.response.content = encrypt(packb(body)) - -addons = [ - APIIntereceptor() -]