Skip to content

Instantly share code, notes, and snippets.

@drdaxxy
Last active May 23, 2019 22:08
Show Gist options
  • Save drdaxxy/6f60d2022035ebf9f3ffd4cd56a94b9e to your computer and use it in GitHub Desktop.
Save drdaxxy/6f60d2022035ebf9f3ffd4cd56a94b9e to your computer and use it in GitHub Desktop.
MAGES. engine sprite assembler. I didn't write this.
#!/usr/bin/env python3
# Steins;Gate 0 Sprite Assembler v0.4
# Assembling sprites using their definitions in LAY files.
# Author: Nimms <nimms@ya.ru>
# 0.4 + edit by DrDaxxy
# support little endian files (Windows builds)
# 0.4:
# [FIX] wildcards: previously evaluation only occured for the last argument;
# now it is done for all of them
# 0.3:
# [ADD] creating separate directories for every processed file
# [FIX] handling the cases where LAY doesn't have 00000001 and/or 2000000x
# states at all
# [FIX] handling the cases where 40000x0y doesn't have matching 2000000x
# (taking previous)
# [FIX] optimization
# 0.2:
# [FIX] arguments: now assuming that LAY file has an underscore at the end
# of its name
# 0.1:
# initial release
import sys
import glob
import os
import struct
import codecs
from PIL import Image
# size of blocks that build the sprite
BLOCK_SIZE = 32
# size of the screen and also the resulting image;
# use the screen resolution which the game uses
SCREEN_WIDTH = 1920
SCREEN_HEIGHT = 1080
# endianness, depends on target platform (> = big, < = little)
BYTE_ORDER = '<'
args = sys.argv[1:]
if len(sys.argv) == 0:
args = glob.glob('*.lay')
else:
for i, arg in enumerate(args):
if '*' in arg:
args = args[:i] + glob.glob(arg) + args[i+1:]
# empty image
empty_im = Image.new('RGBA', (SCREEN_WIDTH, SCREEN_HEIGHT), (0, 0, 0, 0))
for arg in args:
arg = os.path.abspath(os.path.splitext(arg)[0])
if arg[-1] == '_':
arg = arg[:-1]
arg_basename = os.path.basename(arg)
if not os.path.exists(arg_basename):
os.mkdir(arg_basename)
os.chdir(arg_basename)
print(arg_basename)
states = []
coord_records = []
# reading LAY file; refer to unlay.py if you need the specification
with open(arg + '_.lay', 'rb') as f:
states_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0]
coord_records_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0]
i = 0
while i < states_count:
id_raw = bytearray(f.read(4))
if BYTE_ORDER == '<':
id_raw.reverse()
id = codecs.encode(id_raw, 'hex').decode('ascii')
start = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0]
count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0]
states.append({'id': id, 'start': start, 'count': count})
i += 1
i = 0
while i < coord_records_count:
x1 = int(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0] + 1)
y1 = int(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0] + 1)
x2 = int(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0] - 1)
y2 = int(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0] - 1)
coord_records.append((x1, y1, x2, y2))
i += 1
with Image.open(arg + '.png') as image:
# base state: the state that is used as a base image for others
basestate_im = None
face_images = {}
i = 0
while i < states_count:
s = states[i]
s_fname = '{}_{}.png'.format(arg_basename, s['id'])
print(s['id'])
if os.path.isfile(s_fname):
i += 1
continue
to_save = True
to_close = True
if s['id'][0] == '0':
# base state (00000001)
s_im = basestate_im = empty_im.copy()
if states_count > 1:
to_save = False
to_close = False
elif s['id'][0] == '2':
# faces (2000000x)
if basestate_im is None:
im = empty_im.copy()
else:
im = basestate_im.copy()
s_im = face_images[s['id'][-1]] = im
to_save = False
to_close = False
elif s['id'][0] == '4':
# mouths (40000x0y)
if len(face_images) == 0:
if basestate_im is None:
s_im = empty_im.copy()
else:
s_im = basestate_im.copy()
else:
face_id = s['id'][5]
# for Faris: she doesn't have 20000002 but has 4000020x;
# taking previous face
if not face_id in face_images:
face_id = str(int(face_id) - 1)
s_im = face_images[face_id].copy()
else:
# other states
# in fact, only 5010000x which is Daru's nose bleeding
s_im = empty_im.copy()
j = s['start']
while j < s['start'] + s['count']:
c = coord_records[j]
in_box = (c[2], c[3], c[2] + BLOCK_SIZE, c[3] + BLOCK_SIZE)
out_box = (SCREEN_WIDTH//2 + c[0], SCREEN_HEIGHT//2 + c[1])
region = image.crop(in_box)
s_im.paste(region, out_box, region)
j += 1
if to_save:
s_im.save(s_fname)
if to_close:
s_im.close()
i += 1
for im in face_images.values():
im.close()
if not basestate_im is None:
basestate_im.close()
print()
os.chdir('..')
@viliml
Copy link

viliml commented May 23, 2019

@Violettte

The simplest way to fix it would be to save every image. You can then only call that version with those sprites that have no mouths to avoid extra incomplete sprites in the output.

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