Skip to content

Instantly share code, notes, and snippets.

@Kyuuhachi
Last active April 7, 2024 15:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kyuuhachi/42b6acd38a99f7cc8d924286617a9c02 to your computer and use it in GitHub Desktop.
Save Kyuuhachi/42b6acd38a99f7cc8d924286617a9c02 to your computer and use it in GitHub Desktop.
Ys I/II/Origin/VI extractor
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