diff --git a/app.py b/app.py index 49bb22a..4e9ee5a 100644 --- a/app.py +++ b/app.py @@ -359,11 +359,11 @@ class AnimeWwise(QMainWindow): files = [] if self.folders["input"]: if self.pckSubFold.isChecked(): - files = [os.path.join(root, f) for root, dirs, files_in_dir in os.walk(self.folders["input"]) for f in files_in_dir if f.endswith(".pck")] + files = [os.path.join(root, f) for root, dirs, files_in_dir in os.walk(self.folders["input"]) for f in files_in_dir if f.endswith(".pck") or f.endswith(".chk")] else: - files = [os.path.join(self.folders["input"], f) for f in os.listdir(self.folders["input"]) if f.endswith(".pck")] + files = [os.path.join(self.folders["input"], f) for f in os.listdir(self.folders["input"]) if f.endswith(".pck") or f.endswith(".chk")] elif self.loadType == "file": - path = QFileDialog.getOpenFileName(self, "Select .pck File", "", "PCK Files (*.pck)", options=QFileDialog.Options()) + path = QFileDialog.getOpenFileName(self, "Select audio file", "", "PCK Files (*.pck);; CHK Files (*.chk)", options=QFileDialog.Options()) self.folders["input"] = os.path.dirname(path[0]) files = [path[0]] @@ -554,7 +554,8 @@ class AnimeWwise(QMainWindow): "path": path[:-1], "source": meta["source"], "offset": meta["offset"], - "size": meta["size"] + "size": meta["size"], + "original_name": meta["original_name"] } # misc diff --git a/extract.py b/extract.py index 57b581e..13a77c8 100644 --- a/extract.py +++ b/extract.py @@ -6,6 +6,7 @@ import tempfile import wavescan import platform import subprocess +from vfs import decrypt from mapper import Mapper from allocator import Allocator from filereader import FileReader @@ -85,8 +86,7 @@ class WwiseExtract: self.get_wems(data, os.path.basename(_input), hdiff, os.path.relpath(_input, start=base_path)) def get_wems(self, data, filename, hdiff, relpath): - reader = FileReader(io.BytesIO(data), "little") - files = wavescan.get_data(reader, filename) + files = wavescan.get_data(data, filename) if hdiff is not None: with open(hdiff, "rb") as f: @@ -143,8 +143,7 @@ class WwiseExtract: f.write(data) f.close() - reader = FileReader(io.BytesIO(data), "little") - files = wavescan.get_data(reader, source_name) + files = wavescan.get_data(data, source_name) working_dir.cleanup() @@ -167,7 +166,7 @@ class WwiseExtract: # banks = json.loads(handle.read()) # handle.close() - for file in files: + def process_file(file): if mapper is not None: key = mapper.get_key(file[0].split(".")[0]) @@ -183,14 +182,15 @@ class WwiseExtract: "source": relpath, "size": file[2], "offset": file[1], + "original_name": file[0], "metadata": {} } wem_data = data[file_data["offset"]:file_data["offset"]+file_data["size"]] - parsed_wem = wwise.parse_wwise(FileReader(io.BytesIO(wem_data), "little", name=f"{file[3]}:{file[0]}:{file[1]}")) + parsed_wem = wwise.parse_wwise(wem_data, f"{file[3]}:{file[0]}:{file[1]}", file[0]) if not parsed_wem: - continue + return file_data["metadata"] = parsed_wem @@ -228,6 +228,12 @@ class WwiseExtract: if "unmapped" not in temp: temp["unmapped"] = {"folders": {}, "files": []} temp["unmapped"]["files"].append([file[0], file_data]) + + pos = 0 + for file in files: + process_file(file) + pos += 1 + self.update_progress(pos, len(files), 1) self.file_structure = base @@ -316,7 +322,22 @@ class WwiseExtract: file["source"] = file["source"].split(" (hdiff)")[0] data = self.allocator.read_at(file["source"], file["offset"], file["size"]) - + + if data[0:4] not in [b"RIFF", b"RIFX"]: + # file may be vfs encrypted + data = bytearray(data) + wem_id = 0 + try: + wem_id = int(file["original_name"][:-4]) + except ValueError: + try: + wem_id = int(file["original_name"][:-4], 16) + except ValueError: + continue + decrypt(data, 0, len(data), wem_id, 0) + if data[0:4] not in [b"RIFF", b"RIFX"]: + continue + filepath = path("/".join(file["path"]), file["name"]) fullpath = path(output, filepath) os.makedirs(os.path.dirname(fullpath), exist_ok=True) diff --git a/mapper.py b/mapper.py index fa16581..34cf0b4 100644 --- a/mapper.py +++ b/mapper.py @@ -31,7 +31,8 @@ class Mapper: games = { "hk4e": "Genshin", "hkrpg": "Star Rail", - "nap": "Zenless Zone Zero" + "nap": "Zenless Zone Zero", + "beyond": "Arknights Endfield" # more later } diff --git a/maps/beyond.map b/maps/beyond.map new file mode 100644 index 0000000..82438cc Binary files /dev/null and b/maps/beyond.map differ diff --git a/version.json b/version.json index 6686387..968aec9 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "version": 222, - "mapsVersion": 122, + "mapsVersion": 123, "maps": [ { "name": "hk4e.map", @@ -16,6 +16,11 @@ "name": "nap.map", "game": "Zenless Zone Zero", "version": "2.2" + }, + { + "name": "beyond.map", + "game": "Arknights Endfield", + "version": "1.0" } ] } \ No newline at end of file diff --git a/vfs.py b/vfs.py new file mode 100644 index 0000000..5a6b9ef --- /dev/null +++ b/vfs.py @@ -0,0 +1,53 @@ +# decrypt and parse endfield's .chk files for audio + +def decrypt(data, offset, count, seed, fileOffset): + keySeed = seed + (fileOffset >> 2) + + dataIndex = offset + remaining = count + alignement = fileOffset & 3 + + # head + if alignement != 0: + keyValue = key(keySeed) + toAlign = min(4 - alignement, remaining) + + for i in range(toAlign): + if dataIndex >= offset + count: + break + bytePos = alignement + i + data[dataIndex] ^= (keyValue >> (bytePos * 8)) & 0xFF + dataIndex += 1 + + remaining -= toAlign + keySeed += 1 + + # body + nBlocks = remaining // 4 + + for i in range(nBlocks): + keyValue = key(keySeed) + dataValue = int.from_bytes(data[dataIndex:dataIndex+4], "little") ^ keyValue + + data[dataIndex] = dataValue & 0xFF + data[dataIndex+1] = (dataValue >> 8) & 0xFF + data[dataIndex+2] = (dataValue >> 16) & 0xFF + data[dataIndex+3] = (dataValue >> 24) & 0xFF + + dataIndex += 4 + keySeed += 1 + + # tail + trailing = remaining & 3 + if trailing > 0: + keyValue = key(keySeed) + for i in range(trailing): + data[dataIndex] ^= (keyValue >> (i * 8)) & 0xFF + dataIndex += 1 + +def key(seed): + k = ((((seed & 0xFF) ^ 0x9C5A0B29) & 0xFFFFFFFF) * 81861667) & 0xFFFFFFFF + k = ((k ^ ((seed >> 8) & 0xFF)) * 81861667) & 0xFFFFFFFF + k = ((k ^ ((seed >> 16) & 0xFF)) * 81861667) & 0xFFFFFFFF + k = ((k ^ (seed >> 24) & 0xFF) * 81861667) & 0xFFFFFFFF + return k diff --git a/wavescan.py b/wavescan.py index af33961..388d5ae 100644 --- a/wavescan.py +++ b/wavescan.py @@ -1,7 +1,10 @@ # Custom rewrite of the Wwise AKPK packages extractor, original by Nicknine and bnnm import os +import io import traceback from bnk import bnk2wem +from vfs import decrypt +from filereader import FileReader reader = None @@ -10,7 +13,7 @@ wwise_data = [] filename = "" -def get_data(_reader, _filename): +def get_data(data, _filename): global wwise_data global bank_version global reader @@ -18,10 +21,32 @@ def get_data(_reader, _filename): filename = _filename wwise_data = [] - reader = _reader + reader = FileReader(io.BytesIO(data), "little") + vfs = False # check file - if reader.ReadBytes(4) != b"AKPK": + magic = reader.ReadBytes(4) + if magic == b":)xD": + # file is endfield VFS + print("file is VFS !!") + vfs = True + reader.SetBufferPos(4) + header_size = reader.ReadUInt32() + print(header_size) + reader.SetBufferPos(0) + header = bytearray(reader.ReadBytes(header_size+8)) + decrypt(header, 12, header_size - 4, header_size, 0) + # recreate file + dec_data = bytearray() + dec_data += header + dec_data += data[header_size+8:] + dec_data[0:4] = b"AKPK" + dec_data[8:12] = (1).to_bytes(4, "little") + data = dec_data + reader = FileReader(io.BytesIO(data), "little") # reset reader + magic = reader.ReadBytes(4) + + if magic != b"AKPK": # file.close() raise Exception("not a valid audio file") diff --git a/wwise.py b/wwise.py index 07e4a90..9d4acdf 100644 --- a/wwise.py +++ b/wwise.py @@ -1,5 +1,11 @@ # wwise riff header parser # thanks to hcs and bnnm work +import io +from vfs import decrypt +from filereader import FileReader + +def parse_wwise(data, name, fid): + reader = FileReader(io.BytesIO(data), "little", name=name) def parse_wwise(reader): # default meta config @@ -27,6 +33,28 @@ def parse_wwise(reader): header = reader.ReadBytes(4) + if header not in ["RIFF", "RIFX"]: + # file may be vfs encrypted, however this is painfully slow so by default it will skip this and return no metadata + + if False: # set to true to parse metadata + data = bytearray(data) + wem_id = 0 + try: + wem_id = int(fid[:-4]) + except ValueError: + try: + wem_id = int(fid[:-4], 16) + except ValueError: + return None + decrypt(data, 0, len(data), wem_id, 0) + if data[0:4] not in [b"RIFF", b"RIFX"]: + print(f"[WARNING] invalid header {header} at {reader.GetName()}, assuming unreadable") + return None + reader = FileReader(io.BytesIO(data), "little", name=name) # reset reader + header = reader.ReadBytes(4) + else: + return metadata + # endian check header if header == b"RIFX": reader.endianness = "big" @@ -52,7 +80,7 @@ def parse_wwise(reader): while reader.GetBufferPos() < reader.GetStreamLength(): chunk_type = reader.ReadBytes(4) - if chunk_type not in [b"fmt ", b"JUNK", b"data", b"akd ", b"cue ", b"LIST", b"smpl"]: + if chunk_type not in [b"fmt ", b"JUNK", b"data", b"akd ", b"cue ", b"LIST", b"smpl", b"hash", b"seek"]: print(f"[WARNING] unexpected chunk {chunk_type} at {reader.GetName()}") formatted_chunk_type = chunk_type.decode("utf-8").replace(" ", "")