Skip to content

Instantly share code, notes, and snippets.

@blueskythlikesclouds
Last active February 23, 2018 14:47
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 blueskythlikesclouds/7ad60a813d1735d71edda9d4f4eb0f1a to your computer and use it in GitHub Desktop.
Save blueskythlikesclouds/7ad60a813d1735d71edda9d4f4eb0f1a to your computer and use it in GitHub Desktop.
Sonic Mania Sprite Ripper
# Written by Skyth
import struct
import os
import sys
from PIL import Image
import images2gif
def mkdir(path):
if not os.path.exists(path):
os.mkdir(path)
def readUInt(f):
return struct.unpack("<I", f.read(4))[0]
def readInt(f):
return struct.unpack("<i", f.read(4))[0]
def readUShort(f):
return struct.unpack("<H", f.read(2))[0]
def readShort(f):
return struct.unpack("<h", f.read(2))[0]
def readByte(f):
return struct.unpack("B", f.read(1))[0]
def readSByte(f):
return struct.unpack("b", f.read(1))[0]
def readString(f):
length = readByte(f)
return f.read(length)[:-1]
def makeBox(width, height):
return ((0, 0), (width, height))
def combineBox(box1, box2):
return (
(min(box1[0][0], box2[0][0]),
min(box1[0][1], box2[0][1])),
(max(box1[1][0], box2[1][0]),
max(box1[1][1], box2[1][1])))
def moveBox(box, val):
return (
(box[0][0] + val[0],
box[0][1] + val[1]),
(box[1][0] + val[0],
box[1][1] + val[1]))
def boxWidth(box):
return box[1][0] - box[0][0]
def boxHeight(box):
return box[1][1] - box[0][1]
def convert(path):
rootDir = os.path.dirname(path)
outDir = os.path.splitext(path)[0]
mkdir(outDir)
f = open(path, "rb")
signature = readUInt(f)
totalFrameCount = readUInt(f)
spriteSheetsCount = readByte(f)
spriteSheets = []
for i in xrange(spriteSheetsCount):
spriteSheet = readString(f)
spriteSheetPath = os.path.join(os.path.dirname(path), "..", spriteSheet)
if not os.path.exists(spriteSheetPath):
spriteSheetPath = os.path.join(rootDir, os.path.basename(spriteSheet))
if not os.path.exists(spriteSheetPath):
spriteSheets.append(None)
else:
spriteSheets.append(Image.open(spriteSheetPath))
print("Sprite Sheet: {}".format(spriteSheet))
hitboxesCount = readByte(f)
for i in xrange(hitboxesCount):
print("Hitbox: {}".format(readString(f)))
animationsCount = readUShort(f)
for i in xrange(animationsCount):
name = readString(f)
print("Animation: {}".format(name))
if "/" in name:
name = name.replace("/", "&")
frameCount = readUShort(f)
speed = readShort(f)
loopIndex = readByte(f)
flags = readByte(f)
outputGifFile = os.path.join(outDir, name)
frames = []
box = makeBox(0, 0)
for j in xrange(frameCount):
spriteSheetIndex = readByte(f)
if spriteSheetIndex >= spriteSheetsCount or spriteSheets[spriteSheetIndex] == None:
print "Unhandled case! No conversion will be done."
return
duration = readShort(f)
identifier = readShort(f)
x = readUShort(f)
y = readUShort(f)
width = readUShort(f)
height = readUShort(f)
originX = readShort(f)
originY = readShort(f)
frameBox = makeBox(width, height)
frameBox = moveBox(frameBox, (originX, originY))
frameSpeed = 0
if duration > 0:
frameSpeed = (speed * 64) / duration
if frameSpeed > 0:
frameSpeed = 1024 / frameSpeed
frames.append((
spriteSheetIndex,
x, y, width, height,
originX, originY,
frameBox, frameSpeed / 960.0))
box = combineBox(box, frameBox)
for h in xrange(hitboxesCount):
left = readShort(f)
top = readShort(f)
right = readShort(f)
bottom = readShort(f)
if len(frames):
imgWidth = boxWidth(box)
imgHeight = boxHeight(box)
images = []
for frame in frames:
imgCrop = spriteSheets[frame[0]].crop((
frame[1],
frame[2],
frame[1] + frame[3],
frame[2] + frame[4]))
img = Image.new("P", (imgWidth, imgHeight))
img.putpalette(imgCrop.getpalette())
img.paste(imgCrop, (frame[7][0][0] - box[0][0], frame[7][0][1] - box[0][1]))
images.append(img)
images2gif.writeGif(outputGifFile + ".gif", images, duration=[x[8] for x in frames], subRectangles=False)
f.close()
if __name__ == "__main__":
if len(sys.argv) < 2:
print("maniaAnim.py [*.bin]")
else:
convert(sys.argv[1])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment