Skip to content

Instantly share code, notes, and snippets.

@Kyuuhachi
Created August 23, 2023 15:01
Show Gist options
  • Save Kyuuhachi/e4c138c0f7913863feb70f175a505190 to your computer and use it in GitHub Desktop.
Save Kyuuhachi/e4c138c0f7913863feb70f175a505190 to your computer and use it in GitHub Desktop.
Zwei II and Gurumin tools
# 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__()
@Tenome
Copy link

Tenome commented Aug 21, 2024

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.

@Kyuuhachi
Copy link
Author

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.

@Tenome
Copy link

Tenome commented Aug 21, 2024

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.

@Tenome
Copy link

Tenome commented Aug 21, 2024

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).

@Kyuuhachi
Copy link
Author

I highly doubt that they're related. Falcom's always loved inventing their own file formats.

@Tenome
Copy link

Tenome commented Aug 21, 2024

I figured, just wanted to check. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment