Last active
May 23, 2019 22:08
-
-
Save drdaxxy/6f60d2022035ebf9f3ffd4cd56a94b9e to your computer and use it in GitHub Desktop.
MAGES. engine sprite assembler. I didn't write this.
This file contains hidden or 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
#!/usr/bin/env python3 | |
# Steins;Gate 0 Sprite Assembler v0.4 | |
# Assembling sprites using their definitions in LAY files. | |
# Author: Nimms <[email protected]> | |
# 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('..') |
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
was messing a bit with that script and realised that it actually has an edge case with ruka's sprites where her hands are in front of her mouth(_gld, _gmd and all of the other ones ending in "d") the assembler can indeed read the layout file but cant build the sprites because i think its the only time where theres base (00000001) and faces (2000000X) but no mouths (4000000X). Attempted fixing it but i suck at python... thought id at least report this if someone else tried using the script