diff --git a/.vscode/launch.json b/.vscode/launch.json index 87a3572..e2a8acd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -163,8 +163,9 @@ "module": "sssekai", "args": [ "rla2json", + "--dump-audio", "D:\\proseka_reverse\\assets\\streaming_live\\archive\\1st_live_vbs-1", - "D:\\proseka_reverse\\assets\\streaming_live\\archive\\1st_live_vbs-1_src" + "D:\\proseka_reverse\\assets\\streaming_live\\archive\\1st_live_vbs-1_hca" ], "justMyCode": true }, diff --git a/sssekai/__init__.py b/sssekai/__init__.py index 0411939..6769ae7 100644 --- a/sssekai/__init__.py +++ b/sssekai/__init__.py @@ -1,5 +1,5 @@ __VERSION_MAJOR__ = 0 __VERSION_MINOR__ = 3 -__VERSION_PATCH__ = 5 +__VERSION_PATCH__ = 6 __version__ = '%s.%s.%s' % (__VERSION_MAJOR__,__VERSION_MINOR__,__VERSION_PATCH__) diff --git a/sssekai/__main__.py b/sssekai/__main__.py index 0c23e8f..f976dcd 100644 --- a/sssekai/__main__.py +++ b/sssekai/__main__.py @@ -82,8 +82,10 @@ def write(__s): spineextract_parser.set_defaults(func=main_spineextract) # rla2json rla2json_parser = subparsers.add_parser('rla2json', help='''Read streaming_live/archive files and dump their information to JSON''') - 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.add_argument('infile', type=str, help='input file. either a streaming_live bundle or a zip file with the same file hierarchy (i.e. containing sekai.rlh, sekai_xx_xxxxxx.rla files)') + rla2json_parser.add_argument('outdir', type=str, help='output directory. RLA files in JSON format will be saved, unless otherwise specified.') + rla2json_parser.add_argument('--dump-audio', action='store_true', help='dump raw HCA audio data instead of the JSON data. Use https://github.com/mos9527/sssekai_streaming_hca_decoder to decode them.') + rla2json_parser.add_argument('--no-parallel', action='store_true', help='disable parallel processing') rla2json_parser.set_defaults(func=main_rla2json) # apphash apphash_parser = subparsers.add_parser('apphash', help='''Download/extract game AppHash values''') diff --git a/sssekai/entrypoint/rla2json.py b/sssekai/entrypoint/rla2json.py index 57bce6b..7a9f33e 100644 --- a/sssekai/entrypoint/rla2json.py +++ b/sssekai/entrypoint/rla2json.py @@ -8,37 +8,67 @@ from UnityPy.enums import ClassIDType from sssekai.fmt.rla import read_rla from tqdm import tqdm +import zipfile, base64 logger = getLogger(__name__) - -def worker_job(sname, version, script): +def dump_json_job(sname, version, script): rla = read_rla(BytesIO(script), version) dump(rla, open(sname + '.json', 'w', encoding='utf-8'), indent=4, ensure_ascii=False) +def dump_audio_job(sname, version, script): + rla = read_rla(BytesIO(script), version) + for tick, data in rla.items(): + data = data.get('SoundData', None) + if data: + for i, b64data in enumerate(data): + fname = sname + '_%d_%d.hca' % (i, tick) + raw_data = base64.b64decode(b64data['data']) + with open(fname, 'wb') as f: f.write(raw_data) + def main_rla2json(args): - with open(args.infile,'rb') as f: - env = load_assetbundle(f) + with open(args.infile,'rb') as f: datas = dict() - for obj in env.objects: - if obj.type in {ClassIDType.TextAsset}: - data = obj.read() - datas[data.name] = data + if f.read(2) == b'PK': + f.seek(0) + with zipfile.ZipFile(f, 'r') as z: + for name in z.namelist(): + with z.open(name) as zf: + datas[name] = zf.read() + else: + f.seek(0) + rla_env = load_assetbundle(f) + for obj in rla_env.objects: + if obj.type in {ClassIDType.TextAsset}: + data = obj.read() + datas[data.name] = data.script.tobytes() header = datas.get('sekai.rlh', None) assert header, "RLH Header file not found!" makedirs(args.outdir, exist_ok=True) - header = loads(header.text) + header = loads(header.decode('utf-8')) + splitSeconds = header['splitSeconds'] + dump(header, open(path.join(args.outdir, 'sekai.rlh.json'), 'w', encoding='utf-8'), indent=4, ensure_ascii=False) version = tuple(map(int, header['version'].split('.'))) - splitSeconds = header['splitSeconds'] logger.info('Version: %d.%d' % version) logger.info('Count: %d' % len(header['splitFileIds'])) + + worker_job = dump_json_job + if args.dump_audio: + worker_job = dump_audio_job + logger.info('Dumping RLA raw HCA audio data') + else: + logger.info('Dumping RLA data to JSON') with ProcessPoolExecutor() as executor: - logger.info('Dumping RLA data with %d processors' % executor._max_workers) + if not args.no_parallel: + logger.info('Dumping RLA data with %d processors' % executor._max_workers) futures = [] for sid in header['splitFileIds']: sname = 'sekai_%2d_%08d' % (splitSeconds, sid) - script = datas[sname + '.rla'].script.tobytes() - futures.append(executor.submit(worker_job, path.join(args.outdir,sname), version, script)) + script = datas[sname + '.rla'] + if args.no_parallel: + worker_job(path.join(args.outdir,sname), version, script) + else: + futures.append(executor.submit(worker_job, path.join(args.outdir,sname), version, script)) finsihed_futures = set() with tqdm(total=len(futures)) as pbar: while len(finsihed_futures) < len(futures):