Created
August 23, 2023 15:01
-
-
Save Kyuuhachi/e4c138c0f7913863feb70f175a505190 to your computer and use it in GitHub Desktop.
Zwei II and Gurumin tools
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
# 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__() |
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
# 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__() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment