Skip to content

Instantly share code, notes, and snippets.

@Kyuuhachi
Created August 23, 2023 15:01
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/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__()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment