-
-
Save Kyuuhachi/e4c138c0f7913863feb70f175a505190 to your computer and use it in GitHub Desktop.
# Deciphers ITM files into their raw form (usually a bmp, wav, or txt). | |
# Zwei II has an extra level of scrambling on its text files, so use --zwei for that. | |
# Gurumin's sound.itm is not enciphered at all, it's just a binary file. | |
from pathlib import Path | |
import struct | |
import numpy as np | |
Buf = np.ndarray[int, np.dtype[np.uint8]] | |
# decode, decode2, and decode3 are all involutions | |
def decode(path: Path, data: Buf) -> Buf: | |
basename = Path(path).stem.encode() | |
key1 = b"GURUguruMinmIN_" | |
key2 = bytearray(b"!~_Q#xDRgd-d&yfg&'(8)(5(594er2f4asf5f6e5f4s5fvwyjk%fgqTUCFD4568r") # )) | |
key2[8:16] = basename[:8].upper().ljust(8, b"\0") | |
import numpy as np | |
key = np.fromiter([0xFF & ~((key1[i % 15] + (i % 8)) * key2[i % 64]) for i in range(960)], dtype=np.uint8) | |
return data ^ np.resize(key, data.shape) | |
def checksum(data: Buf) -> tuple[np.uint32, np.uint32]: | |
return ( | |
np.sum(data, dtype=np.uint32), | |
np.bitwise_xor.reduce(data, initial=56879, dtype=np.uint32), | |
) | |
def decode2(data: Buf) -> Buf: | |
key = ~np.arange(256, dtype=np.uint8) | |
return data ^ np.resize(key, data.shape) | |
def decode3(data: Buf) -> Buf: | |
return (data << 4) | (data >> 4) | |
def read(path: Path, input: bytes) -> Buf: | |
_a, b, c = struct.unpack("III", input[-12:]) | |
data = decode(path, np.frombuffer(input[:-12], np.uint8)) | |
check = checksum(data) | |
assert (b, c) == check, ((b, c), check) | |
return data | |
def __main__(): | |
import argparse | |
argp = argparse.ArgumentParser() | |
argp.add_argument("files", metavar="file", nargs="+", type=Path) | |
argp.add_argument("--zwei", "-z") | |
argp.add_argument("--outdir", "-o", type=Path) | |
args = argp.parse_args() | |
for p in args.files: | |
print(p, end=": ", flush=True) | |
data = read(p, p.read_bytes()) | |
try: | |
outp = args.outdir / p.name if args.outdir else p | |
bs = data.tobytes() | |
if bs.startswith(b"BM"): | |
outp = outp.with_suffix(".bmp") | |
outp.write_bytes(bs) | |
elif bs.startswith(b"MV"): | |
outp = outp.with_suffix(".mmv") | |
outp.write_bytes(bs) | |
elif bs.startswith(b"RIFF"): | |
outp = outp.with_suffix(".wav") | |
outp.write_bytes(bs) | |
else: | |
try: | |
if args.zwei: | |
data = decode3(decode2(data)) | |
text = data.tobytes().decode("cp932") | |
except UnicodeDecodeError as e: | |
outp.with_suffix(".bin").write_bytes(data) | |
raise | |
else: | |
outp = outp.with_suffix(".txt") | |
outp.write_text(text) | |
except Exception as e: | |
print(e) | |
else: | |
print(outp) | |
if __name__ == "__main__": __main__() |
# Converts MMV files to GIF | |
from pathlib import Path | |
import numpy as np | |
from PIL import Image | |
import struct | |
import typing as T | |
# PIL messes up the palette in some weird way. I gave you a palette, use it. | |
from PIL import GifImagePlugin | |
def _normalize_palette(im, palette, info): | |
im.palette.palette = palette | |
return im | |
GifImagePlugin._normalize_palette = _normalize_palette | |
def unpack(f: T.IO[bytes], fmt: str): | |
return struct.unpack("<" + fmt, f.read(struct.calcsize("<" + fmt))) | |
def read(f: T.IO[bytes]) -> list[Image.Image]: | |
head, w, h = unpack(f, "2sHH") | |
assert head == b"MV", head | |
colors = np.array(unpack(f, "256I"), np.uint32) | |
palette = colors[...,None].view("u1")[:,[2,1,0]].tobytes() | |
assert len(palette) == 768 | |
frames = [] | |
prev = b"" | |
while True: | |
k = f.read(4) | |
if not k: break | |
chunklen = int.from_bytes(k, "little") | |
chunk = read_chunk(f.read(chunklen), prev) | |
prev = chunk | |
imgbuf = np.array(chunk, np.uint8).reshape(h, w) | |
img = Image.fromarray(imgbuf, "P") | |
img.info["palette"] = palette | |
frames.append(img) | |
return frames | |
def read_chunk(bs: bytes, prev: bytes) -> bytes: | |
i = 0 | |
def u1(): | |
nonlocal i | |
i += 1 | |
return bs[i-1] | |
out = bytearray() | |
while i < len(bs): | |
match u1(): | |
case 0xFC: | |
a = u1() | |
out.extend(memoryview(prev)[len(out):len(out)+a]) | |
case 0xFD: | |
a, b = u1(), u1() | |
out.extend([b] * a); | |
case b: | |
out.append(b) | |
return out | |
def __main__(): | |
import argparse | |
argp = argparse.ArgumentParser() | |
argp.add_argument("files", metavar="file", nargs="+", type=Path) | |
argp.add_argument("--outdir", "-o", type=Path) | |
args = argp.parse_args() | |
for p in args.files: | |
print(p, end=": ", flush=True) | |
with p.open("rb") as f: | |
try: | |
outp = args.outdir / p.name if args.outdir else p | |
outp = outp.with_suffix(".gif") | |
frames = read(f) | |
frames[0].info["loop"] = 0 | |
frames[0].save(outp, save_all=True, append_images=frames[1:]) | |
except Exception as e: | |
print(e) | |
else: | |
print(outp) | |
if __name__ == "__main__": __main__() |
match
requires python 3.10, released in 2021. This tool currently doesn't support packing back into mmv, but it should serve as sufficient documentation of the format if you want to make your own.
Thanks, this was also due to my Python being out of date. I'm still curious as to how I extracted it before (I think I was using the JP tools). My previous notes say that falcnvrt worked, but when I tried it just now it says it's an unrecognized format. If I figure out what I did last time, I'll post a comment here for posterity.
I think I see what happened, falcnvrt targets the .arc archives of the original JP Gurumin and successfully extracts the .mmv files inside as pngs. I feel like I also extracted the English versions too before, so I'll keep looking.
Sorry to keep bugging you, but do you know if the MMV files Falcom uses are the same as Sony MicroMV files? I'm skeptical that it is, since that's a camcorder format, but it also produces animated slideshows in a similar manner... Are there any notes in the Falcom modding community about it? I can't find anything. I also tried making a new .mmv with Sony's old editor, Movieshaker, but the game can't read it (and they're much larger anyway, so I doubt they were using that).
I highly doubt that they're related. Falcom's always loved inventing their own file formats.
I figured, just wanted to check. Thanks.
Does mmv.py work with Gurumin's .mmvfiles? When I tried it on abyss.mmv, it gave me an invalid syntax error on line 50
u1()
I swear I extracted them all with something at one point, but it doesn't seem like I can remember how to do it. They should contain the sprites used for the animated stage names. I'm trying to see if there's a way to extract the images, edit them, and then repack them into mmv files.