Last active
April 7, 2024 15:21
-
-
Save Kyuuhachi/42b6acd38a99f7cc8d924286617a9c02 to your computer and use it in GitHub Desktop.
Ys I/II/Origin/VI extractor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import typing as T | |
from pathlib import Path | |
import zlib | |
import struct | |
try: | |
import numpy as np | |
def decode(data: bytes) -> bytes: | |
data = np.frombuffer(data, dtype=np.ubyte).copy() | |
obf = 0x7C53F961 * 0x3D09 ** (1+np.arange(len(data), dtype=np.uint32)) | |
data -= obf >> 16 | |
return data.tobytes() | |
except ImportError: | |
print("Could not import numpy — falling back to slow method") | |
def decode(data: bytes) -> bytes: | |
k = 0x7C53F961 | |
o = bytearray() | |
for b in data: | |
k *= 0x3D09 | |
o.append((b - (k >> 16)) & 0xFF) | |
return bytes(o) | |
def fnhash(name: bytes) -> int: | |
return sum( | |
(b - 32) * (1<<(i%5*5)) | |
for i, b in enumerate(name) | |
) % 0xFFF1 | |
def extract(na: bytes, ni: bytes, *, verify: bool = False) -> T.Iterator[tuple[str, bytes]]: | |
def take(a: bytes, n: int) -> tuple[bytes, bytes]: return a[:n], a[n:] | |
head, ni = take(ni, 16) | |
head, p2, p3, p4 = struct.unpack("<4sIII", head) | |
assert head == b"NNI\0" | |
toc, ni = take(ni, p2*16) | |
names, ni = take(ni, p3) | |
assert p4 & 0x01 == 0, ".\\core\\nnk_file.cpp: vfs::Startup: No incremental linking support." | |
assert not ni | |
toc = decode(toc) | |
names = decode(names) | |
for hash, size, pos, namepos in struct.iter_unpack("<IIII", toc): | |
name = names[namepos:names.find(b"\0", namepos)] | |
if verify: assert hash == fnhash(name), name | |
name = name.decode("cp932").lower().replace("\\", "/") | |
if (pos, size) == (0, 0): | |
print(f"skipping blank file {name}") | |
continue | |
data = na[pos:pos+size] | |
if name.endswith(".z"): | |
name = name[:-2] | |
try: | |
checksum, usize = struct.unpack("<II", data[:8]) | |
data = zlib.decompress(data[8:]) | |
assert len(data) == usize | |
if verify: assert checksum == zlib.crc32(data) | |
except Exception: | |
print(f"failed to decompress {name}") | |
import traceback | |
traceback.print_exc() | |
yield name, data | |
import argparse | |
argp = argparse.ArgumentParser() | |
argp.add_argument("-o", "--outdir", type=Path, required=True, dest="outdir") | |
argp.add_argument("-v", "--verbose", action="store_true") | |
argp.add_argument("-V", "--verify", action="store_true") | |
argp.add_argument("files", nargs="+", type=Path) | |
def __main__(outdir: Path, verbose: bool, verify: bool, files: list[Path]): | |
for path in files: | |
assert path.suffix in [".na", ".ni"], path | |
na = path.with_suffix(".na").read_bytes() | |
ni = path.with_suffix(".ni").read_bytes() | |
for name, data in extract(na, ni, verify=verify): | |
out = outdir / name | |
if verbose: print(out) | |
out.parent.mkdir(parents=True, exist_ok=True) | |
out.write_bytes(data) | |
if __name__ == "__main__": | |
__main__(**argp.parse_args().__dict__) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment