Last active
May 24, 2019 20:02
-
-
Save viliml/94b88cbe9c2cb33a201e20832aaa744c to your computer and use it in GitHub Desktop.
With much code copying from Nimms <nimms@ya.ru> and anon
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
#!/usr/bin/env python3 | |
import sys | |
import glob | |
import os | |
import struct | |
import codecs | |
from PIL import Image, ImageTk | |
from itertools import product | |
class State: | |
def __init__(self, id, count, start): | |
self.id = id | |
self.count = count | |
self.start = start | |
self.overlays = {} | |
class Overlay: | |
def __init__(self, count, start): | |
self.count = count | |
self.start = start | |
class Sprite: | |
def __init__(self, path, width, height, states, records, table): | |
self.path = path | |
self.width = width | |
self.height = height | |
self.states = states | |
self.records = records | |
self.table = table | |
# endianness, depends on target platform (> = big, < = little) | |
BYTE_ORDER = '<' | |
BLOCK_SIZE = 32 | |
SCREEN_WIDTH = 1920 | |
SCREEN_HEIGHT = 1080 | |
NAMES = {'ama': 'Amanda', 'ayu': 'Ayumi', 'chi': 'Child Yu-no', 'eri': 'Eriko', 'hoj': 'Houjou', 'kan': 'Kanna', 'kao': 'Kaori', 'kon': 'Imperial Guard', 'kou': 'Koudai', 'mio': 'Mio', 'mit': 'Mitsuki', 'osy': 'Marina (Female Guard)', 'rou': 'Ume', 'ryu': 'Ryuuzouji', 'saa': 'Some miner guy', 'sab': 'Some other miner guy', 'ser': 'Sayless', 'sin': 'God-Empress', 'syo': 'Quarry Chief', 'tak': 'Takuya', 'toy': 'Toyotomi', 'yuk': 'Yuuki', 'yun': 'Yu-no'} | |
OVERLAYS = {'L': 'Lips', 'E': 'Eyelids'} | |
VARIANTS = {'': 'Base', | |
'_2': 'Night', '_2b': 'Night B', '_2bc': 'Night B Corpse', '_2bc2': 'Night B Corpse 2', '_2c': 'Night C', '_2d': 'Night D', '_2t': 'Night Telephone', | |
'_3': 'Evening', '_4': 'Rain', | |
'r': 'Reverse', 'rc': 'Reverse C', 'rd': 'Reverse D', 're': 'Reverse E', 't': 'Telephone', 'y': 'Yu-no', | |
'b': 'B', 'b2': 'B Shadow', 'b_2': 'B Night', 'bc': 'B+C', | |
'c': 'C', 'c2': 'C Item', 'c_2': 'C Night', | |
'd': 'D', 'd_2': 'D Night', 'dt': 'D Telephone', | |
'e': 'E', 'e_2': 'E Night', | |
'f': 'F', 'f_2': 'F Night', | |
'g': 'G', 'g_2': 'G Night', | |
'h': 'H', 'h_2': 'H Night', | |
'i': 'I', | |
'j': 'J'} | |
INVERSE = {val: key for key, val in VARIANTS.items()} | |
args = sys.argv[1:] | |
if len(sys.argv) <= 1: | |
args = glob.glob('*.mvl') | |
else: | |
for i, arg in enumerate(args): | |
if '*' in arg: | |
args = args[:i] + glob.glob(arg) + args[i+1:] | |
sprites = {} | |
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) | |
# only do BGs | |
if not any(c.isdigit() for c in arg_basename): | |
continue | |
if arg_basename[-3] != 'h': | |
continue | |
#print(arg_basename) | |
name = arg_basename[4:7] | |
num = arg_basename[-2:] | |
records_count = 0 | |
table_count = 0 | |
states = [] | |
records = [] | |
table = [] | |
table_start = None | |
with open(arg + '_.mvl', 'rb') as f: | |
# 4 chars: MVL1 | |
f.read(4) | |
# uint32: count of states | |
states_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
#print(states_count) | |
# uint32: input width | |
input_width = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
#print(input_width) | |
# 20 zeros | |
f.read(20) | |
# 64 characters | |
shit = str(f.read(64), encoding='ascii').strip('\0') | |
#print(shit) | |
for i in range(states_count): | |
# uint32: output width | |
output_width = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: output height | |
output_height = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: idk | |
idk = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# 4 zeros | |
f.read(4) | |
# uint32: count of coordinates records | |
records_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: beginning of coordinates records | |
records_start = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: count of records to read | |
count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: beginning of records to read | |
start = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# 32 chars: state name | |
id = str(f.read(32), encoding='ascii').strip('\0') | |
if table_start == None: | |
table_start = start | |
start = (start - table_start) // 2 | |
table_count += count | |
if len(id) == 9: | |
states.append(State(id, count, start)) | |
elif len(id) == 11: | |
states[-1].overlays.setdefault(id[-2], []).append(Overlay(count, start)) | |
else: | |
print("weird!") | |
#print(id, count, start, sep='\t') | |
#print() | |
#print(output_width, output_height) | |
#print() | |
#print(records_count) | |
for i in range(records_count): | |
# float32: block position on the screen, X | |
x1 = round(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0])# + 1 | |
# float32: block position on the screen, Y | |
y1 = round(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0])# + 1 | |
# 4 zeros | |
f.read(4) | |
# float32: block position in the image, X | |
x2 = struct.unpack(BYTE_ORDER + 'f', f.read(4))[0]# - 1 | |
# float32: block position in the image, Y | |
y2 = struct.unpack(BYTE_ORDER + 'f', f.read(4))[0]# - 1 | |
#print(x1, y1, x2, y2) | |
records.append((x1, y1, x2, y2)) | |
#print() | |
#print(map_count) | |
for i in range(table_count): | |
(idx,) = struct.unpack(BYTE_ORDER + 'H', f.read(2)) | |
table.append(idx) | |
sprites.setdefault(num, {})[name] = Sprite(arg + '.png', output_width, output_height, states, records, table) | |
bgs = {} | |
for path in glob.glob('bgs/yng_h*.png'): | |
path = os.path.abspath(os.path.splitext(path)[0]) | |
if path[-1] == '_': | |
path = path[:-1] | |
basename = os.path.basename(path) | |
num = basename[5:7] | |
ext = basename[7:] | |
bgs.setdefault(num, []).append(ext) | |
sprites.setdefault(num, {}) | |
import tkinter as tk | |
from tkinter import ttk | |
class Application(ttk.Frame): | |
def __init__(self, master=None): | |
ttk.Frame.__init__(self, master) | |
self.grid() | |
self.createWidgets() | |
self.base_im = None | |
self.display = None | |
self.variant_label = None | |
self.variant_picker = None | |
self.labels = {} | |
self.pickers = {} | |
self.compressed_ims = {} | |
self.labels2s = {} | |
self.scaless = {} | |
def select_bg(self, dummy_event=None): | |
if self.variant_label: | |
self.variant_label.grid_forget() | |
if self.variant_picker: | |
self.variant_picker.grid_forget() | |
for label in self.labels.values(): | |
label.grid_forget() | |
for picker in self.pickers.values(): | |
picker.grid_forget() | |
for compressed_im in self.compressed_ims.values(): | |
compressed_im.close() | |
for labels2 in self.labels2s: | |
for label in labels: | |
label.grid_forget() | |
for scales in self.scaless: | |
for scale in scales: | |
scale.grid_forget() | |
self.variant_label = ttk.Label(self, text='Variant') | |
self.variant_label.grid(row=0,column=1) | |
self.variant_picker = ttk.Combobox(self, state='readonly', values=[VARIANTS[x] for x in bgs[self.number_picker.get()]]) | |
self.variant_picker.bind('<<ComboboxSelected>>', self.redraw) | |
self.variant_picker.set(VARIANTS['']) | |
self.variant_picker.grid(row=1, column=1) | |
self.labels.clear() | |
self.pickers.clear() | |
for i, (name, sprite) in enumerate(sprites[self.number_picker.get()].items()): | |
label = ttk.Label(self, text=NAMES[name]) | |
label.grid(row=0,column=2+2*i, columnspan=2) | |
self.labels[name] = label | |
picker = ttk.Combobox(self, state='readonly', values=[x for x in range(len(sprite.states)+1)]) | |
picker.bind('<<ComboboxSelected>>', self.pick_face) | |
picker.set('0') | |
picker.grid(row=1, column=2+2*i, columnspan=2) | |
self.pickers[name] = picker | |
self.compressed_ims[name] = Image.open(sprite.path) | |
self.labels2s[name] = {} | |
self.scaless[name] = {} | |
self.base_im = Image.open('bgs/yng_h' + self.number_picker.get() + '.png').convert(mode='RGBA') | |
self.redraw() | |
def pick_face(self, dummy_event=None): | |
for i, (name, sprite) in enumerate(sprites[self.number_picker.get()].items()): | |
labels2 = self.labels2s[name] | |
scales = self.scaless[name] | |
for label in labels2.values(): | |
label.grid_forget() | |
for scale in scales.values(): | |
scale.grid_forget() | |
labels2.clear() | |
scales.clear() | |
compressed_im = self.compressed_ims[name] | |
picker = self.pickers[name] | |
x = int(picker.get()) - 1 | |
if x != -1: | |
state = sprite.states[x] | |
for j, (o, l) in enumerate(state.overlays.items()): | |
label = ttk.Label(self, text = OVERLAYS[o]) | |
label.grid(row=2,column=2+2*i+j, columnspan=2//len(state.overlays)) | |
labels2[o] = label | |
scale = ttk.Scale(self, to=len(l)-1, command=self.redraw, length=200) | |
scale.grid(row=3, column=2+2*i+j, columnspan=2//len(state.overlays)) | |
scales[o] = scale | |
self.redraw() | |
def redraw(self, dummy_event=None, *idk): | |
if not self.base_im: | |
return | |
for scales in self.scaless.values(): | |
for scale in scales.values(): | |
if scale.get() != round(scale.get()): | |
scale.set(round(scale.get())) | |
return | |
if self.display: | |
self.display.grid_forget() | |
s_im = Image.alpha_composite(self.base_im, Image.open('bgs/yng_h' + self.number_picker.get() + INVERSE[self.variant_picker.get()] + '.png').convert(mode='RGBA')) | |
for name, sprite in sprites[self.number_picker.get()].items(): | |
compressed_im = self.compressed_ims[name] | |
picker = self.pickers[name] | |
scales = self.scaless[name] | |
x = int(picker.get()) - 1 | |
if x != -1: | |
image = compressed_im | |
s = sprite.states[x] | |
for i in range(s.start, s.start + s.count, 6): | |
c = sprite.records[sprite.table[i]] | |
in_box = (round(c[2] * image.width) - 1, round(c[3] * image.height) - 1, round(c[2] * image.width) + BLOCK_SIZE - 1, round(c[3] * image.height) + BLOCK_SIZE - 1) | |
out_box = (SCREEN_WIDTH//2 + c[0] - 1, SCREEN_HEIGHT//2 + c[1] - 1) | |
region = image.crop(in_box) | |
s_im.paste(region, out_box, region) | |
for o, l in s.overlays.items(): | |
overlay = l[round(scales[o].get())] | |
for i in range(overlay.start, overlay.start + overlay.count, 6): | |
c = sprite.records[sprite.table[i]] | |
in_box = (round(c[2] * image.width) - 1, round(c[3] * image.height) - 1, round(c[2] * image.width) + BLOCK_SIZE - 1, round(c[3] * image.height) + BLOCK_SIZE - 1) | |
out_box = (SCREEN_WIDTH//2 + c[0] - 1, SCREEN_HEIGHT//2 + c[1] - 1) | |
region = image.crop(in_box) | |
s_im.paste(region, out_box, region) | |
if self.height.get(): | |
render = ImageTk.PhotoImage(s_im.resize(((SCREEN_WIDTH * self.height.get()) // SCREEN_HEIGHT, self.height.get()))) | |
else: | |
render = ImageTk.PhotoImage(s_im.copy()) | |
self.display = ttk.Label(self, image = render) | |
self.display.image = render | |
self.display.grid(row=4,column=1,columnspan=1+2*len(sprites[self.number_picker.get()])) | |
s_im.close() | |
def createWidgets(self): | |
ttk.Label(self, text='Height').grid(row=0,column=0) | |
self.height = tk.IntVar() | |
self.height.trace('w', self.redraw) | |
e = ttk.Entry(self, textvariable=self.height) | |
e.grid(row=1,column=0) | |
self.number_picker = ttk.Combobox(self, height=20, state='readonly', values=list(bgs)) | |
self.number_picker.bind('<<ComboboxSelected>>', self.select_bg) | |
self.number_picker.grid(row=2, column=0, rowspan=2, sticky='N') | |
app = Application() | |
app.master.title('YUNO BG Viewer') | |
app.mainloop() |
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
#!/usr/bin/env python3 | |
import sys | |
import glob | |
import os | |
import struct | |
import codecs | |
from PIL import Image, ImageTk | |
from itertools import product | |
class State: | |
def __init__(self, id, count, start): | |
self.id = id | |
self.count = count | |
self.start = start | |
self.overlays = {} | |
class Overlay: | |
def __init__(self, count, start): | |
self.count = count | |
self.start = start | |
class Sprite: | |
def __init__(self, path, width, height, states, records, table): | |
self.path = path | |
self.width = width | |
self.height = height | |
self.states = states | |
self.records = records | |
self.table = table | |
# endianness, depends on target platform (> = big, < = little) | |
BYTE_ORDER = '<' | |
BLOCK_SIZE = 32 | |
SCREEN_WIDTH = 1920 | |
SCREEN_HEIGHT = 1080 | |
NAMES = {'ama': 'Amanda', 'ayu': 'Ayumi', 'chi': 'Child Yu-no', 'eri': 'Eriko', 'hoj': 'Houjou', 'kan': 'Kanna', 'kao': 'Kaori', 'kon': 'Imperial Guard', 'kou': 'Koudai', 'mio': 'Mio', 'mit': 'Mitsuki', 'osy': 'Marina (Female Guard)', 'rou': 'Ume', 'ryu': 'Ryuuzouji', 'saa': 'Some miner guy', 'sab': 'Some other miner guy', 'ser': 'Sayless', 'sin': 'God-Empress', 'syo': 'Quarry Chief', 'tak': 'Takuya', 'toy': 'Toyotomi', 'yuk': 'Yuuki', 'yun': 'Yu-no'} | |
OVERLAYS = {'L': 'Lips', 'E': 'Eyelids'} | |
VARIANTS = {'': 'Base', | |
'_2': 'Alternate', '_2b': 'Alternate B', '_2c': 'Alternate C', '_2d': 'Alternate D', | |
's': 'Sepia', | |
'a': 'A', | |
'b': 'B', 'b2': 'B Shadow', 'b_2': 'B Alternate', 'bl': 'B Light', 'bn': 'B Night', | |
'c': 'C', 'c2': 'C Shadow', 'c_b': 'C Black', 'c_w': 'C White', | |
'd': 'D', 'd_2': 'D Alternate', 'd_b': 'D Black', 'd_w': 'D White', | |
'e': 'E', 'e_2': 'E Alternate', | |
'f': 'F', | |
'g': 'G', | |
'h': 'H', | |
'i': 'I', | |
'j': 'J'} | |
INVERSE = {val: key for key, val in VARIANTS.items()} | |
args = sys.argv[1:] | |
if len(sys.argv) <= 1: | |
args = glob.glob('*.mvl') | |
else: | |
for i, arg in enumerate(args): | |
if '*' in arg: | |
args = args[:i] + glob.glob(arg) + args[i+1:] | |
sprites = {} | |
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) | |
# only do CGs | |
if not any(c.isdigit() for c in arg_basename): | |
continue | |
if arg_basename[-4] == '_': | |
continue | |
#print(arg_basename) | |
name = arg_basename[4:7] | |
num = arg_basename[-4:] | |
records_count = 0 | |
table_count = 0 | |
states = [] | |
records = [] | |
table = [] | |
table_start = None | |
with open(arg + '_.mvl', 'rb') as f: | |
# 4 chars: MVL1 | |
f.read(4) | |
# uint32: count of states | |
states_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
#print(states_count) | |
# uint32: input width | |
input_width = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
#print(input_width) | |
# 20 zeros | |
f.read(20) | |
# 64 characters | |
shit = str(f.read(64), encoding='ascii').strip('\0') | |
#print(shit) | |
for i in range(states_count): | |
# uint32: output width | |
output_width = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: output height | |
output_height = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: idk | |
idk = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# 4 zeros | |
f.read(4) | |
# uint32: count of coordinates records | |
records_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: beginning of coordinates records | |
records_start = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: count of records to read | |
count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: beginning of records to read | |
start = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# 32 chars: state name | |
id = str(f.read(32), encoding='ascii').strip('\0') | |
if table_start == None: | |
table_start = start | |
start = (start - table_start) // 2 | |
table_count += count | |
if len(id) == 9: | |
states.append(State(id, count, start)) | |
elif len(id) == 11: | |
states[-1].overlays.setdefault(id[-2], []).append(Overlay(count, start)) | |
else: | |
print("weird!") | |
#print(id, count, start, sep='\t') | |
#print() | |
#print(output_width, output_height) | |
#print() | |
#print(records_count) | |
for i in range(records_count): | |
# float32: block position on the screen, X | |
x1 = round(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0])# + 1 | |
# float32: block position on the screen, Y | |
y1 = round(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0])# + 1 | |
# 4 zeros | |
f.read(4) | |
# float32: block position in the image, X | |
x2 = struct.unpack(BYTE_ORDER + 'f', f.read(4))[0]# - 1 | |
# float32: block position in the image, Y | |
y2 = struct.unpack(BYTE_ORDER + 'f', f.read(4))[0]# - 1 | |
#print(x1, y1, x2, y2) | |
records.append((x1, y1, x2, y2)) | |
#print() | |
#print(map_count) | |
for i in range(table_count): | |
(idx,) = struct.unpack(BYTE_ORDER + 'H', f.read(2)) | |
table.append(idx) | |
sprites.setdefault(num, {})[name] = Sprite(arg + '.png', output_width, output_height, states, records, table) | |
cgs = {} | |
for path in glob.glob('cgs/yng_*.png'): | |
path = os.path.abspath(os.path.splitext(path)[0]) | |
if path[-1] == '_': | |
path = path[:-1] | |
basename = os.path.basename(path) | |
num = basename[4:8] | |
ext = basename[8:] | |
cgs.setdefault(num, []).append(ext) | |
sprites.setdefault(num, {}) | |
import tkinter as tk | |
from tkinter import ttk | |
class Application(ttk.Frame): | |
def __init__(self, master=None): | |
ttk.Frame.__init__(self, master) | |
self.grid() | |
self.createWidgets() | |
self.base_im = None | |
self.display = None | |
self.variant_label = None | |
self.variant_picker = None | |
self.labels = {} | |
self.pickers = {} | |
self.compressed_ims = {} | |
self.labels2s = {} | |
self.scaless = {} | |
def select_cg(self, dummy_event=None): | |
if self.variant_label: | |
self.variant_label.grid_forget() | |
if self.variant_picker: | |
self.variant_picker.grid_forget() | |
for label in self.labels.values(): | |
label.grid_forget() | |
for picker in self.pickers.values(): | |
picker.grid_forget() | |
for compressed_im in self.compressed_ims.values(): | |
compressed_im.close() | |
for labels2 in self.labels2s: | |
for label in labels: | |
label.grid_forget() | |
for scales in self.scaless: | |
for scale in scales: | |
scale.grid_forget() | |
self.variant_label = ttk.Label(self, text='Variant') | |
self.variant_label.grid(row=0,column=1) | |
self.variant_picker = ttk.Combobox(self, state='readonly', values=[VARIANTS[x] for x in cgs[self.number_picker.get()]]) | |
self.variant_picker.bind('<<ComboboxSelected>>', self.redraw) | |
self.variant_picker.set(VARIANTS['']) | |
self.variant_picker.grid(row=1, column=1) | |
self.labels.clear() | |
self.pickers.clear() | |
for i, (name, sprite) in enumerate(sprites[self.number_picker.get()].items()): | |
label = ttk.Label(self, text=NAMES[name]) | |
label.grid(row=0,column=2+2*i, columnspan=2) | |
self.labels[name] = label | |
picker = ttk.Combobox(self, state='readonly', values=[x for x in range(len(sprite.states)+1)]) | |
picker.bind('<<ComboboxSelected>>', self.pick_face) | |
picker.set('0') | |
picker.grid(row=1, column=2+2*i, columnspan=2) | |
self.pickers[name] = picker | |
self.compressed_ims[name] = Image.open(sprite.path) | |
self.labels2s[name] = {} | |
self.scaless[name] = {} | |
self.base_im = Image.open('cgs/yng_' + self.number_picker.get() + '.png').convert(mode='RGBA') | |
self.redraw() | |
def pick_face(self, dummy_event=None): | |
for i, (name, sprite) in enumerate(sprites[self.number_picker.get()].items()): | |
labels2 = self.labels2s[name] | |
scales = self.scaless[name] | |
for label in labels2.values(): | |
label.grid_forget() | |
for scale in scales.values(): | |
scale.grid_forget() | |
labels2.clear() | |
scales.clear() | |
compressed_im = self.compressed_ims[name] | |
picker = self.pickers[name] | |
x = int(picker.get()) - 1 | |
if x != -1: | |
state = sprite.states[x] | |
for j, (o, l) in enumerate(state.overlays.items()): | |
label = ttk.Label(self, text = OVERLAYS[o]) | |
label.grid(row=2,column=2+2*i+j, columnspan=2//len(state.overlays)) | |
labels2[o] = label | |
scale = ttk.Scale(self, to=len(l)-1, command=self.redraw, length=200) | |
scale.grid(row=3, column=2+2*i+j, columnspan=2//len(state.overlays)) | |
scales[o] = scale | |
self.redraw() | |
def redraw(self, dummy_event=None, *idk): | |
if not self.base_im: | |
return | |
for scales in self.scaless.values(): | |
for scale in scales.values(): | |
if scale.get() != round(scale.get()): | |
scale.set(round(scale.get())) | |
return | |
if self.display: | |
self.display.grid_forget() | |
s_im = Image.alpha_composite(self.base_im, Image.open('cgs/yng_' + self.number_picker.get() + INVERSE[self.variant_picker.get()] + '.png').convert(mode='RGBA')) | |
for name, sprite in sprites[self.number_picker.get()].items(): | |
compressed_im = self.compressed_ims[name] | |
picker = self.pickers[name] | |
scales = self.scaless[name] | |
x = int(picker.get()) - 1 | |
if x != -1: | |
image = compressed_im | |
s = sprite.states[x] | |
for i in range(s.start, s.start + s.count, 6): | |
c = sprite.records[sprite.table[i]] | |
in_box = (round(c[2] * image.width) - 1, round(c[3] * image.height) - 1, round(c[2] * image.width) + BLOCK_SIZE - 1, round(c[3] * image.height) + BLOCK_SIZE - 1) | |
out_box = (SCREEN_WIDTH//2 + c[0] - 1, SCREEN_HEIGHT//2 + c[1] - 1) | |
region = image.crop(in_box) | |
s_im.paste(region, out_box, region) | |
for o, l in s.overlays.items(): | |
overlay = l[round(scales[o].get())] | |
for i in range(overlay.start, overlay.start + overlay.count, 6): | |
c = sprite.records[sprite.table[i]] | |
in_box = (round(c[2] * image.width) - 1, round(c[3] * image.height) - 1, round(c[2] * image.width) + BLOCK_SIZE - 1, round(c[3] * image.height) + BLOCK_SIZE - 1) | |
out_box = (SCREEN_WIDTH//2 + c[0] - 1, SCREEN_HEIGHT//2 + c[1] - 1) | |
region = image.crop(in_box) | |
s_im.paste(region, out_box, region) | |
if self.height.get(): | |
render = ImageTk.PhotoImage(s_im.resize(((SCREEN_WIDTH * self.height.get()) // SCREEN_HEIGHT, self.height.get()))) | |
else: | |
render = ImageTk.PhotoImage(s_im.copy()) | |
self.display = ttk.Label(self, image = render) | |
self.display.image = render | |
self.display.grid(row=4,column=1,columnspan=1+2*len(sprites[self.number_picker.get()])) | |
s_im.close() | |
def createWidgets(self): | |
ttk.Label(self, text='Height').grid(row=0,column=0) | |
self.height = tk.IntVar() | |
self.height.trace('w', self.redraw) | |
e = ttk.Entry(self, textvariable=self.height) | |
e.grid(row=1,column=0) | |
self.number_picker = ttk.Combobox(self, height=20, state='readonly', values=list(cgs)) | |
self.number_picker.bind('<<ComboboxSelected>>', self.select_cg) | |
self.number_picker.grid(row=2, column=0, rowspan=2, sticky='N') | |
app = Application() | |
app.master.title('YUNO CG Viewer') | |
app.mainloop() |
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
#!/usr/bin/env python3 | |
import sys | |
import glob | |
import os | |
import struct | |
import codecs | |
from PIL import Image | |
from itertools import product | |
class State: | |
def __init__(self, id, count, start): | |
self.id = id | |
self.count = count | |
self.start = start | |
self.overlays = {} | |
class Overlay: | |
def __init__(self, count, start): | |
self.count = count | |
self.start = start | |
# endianness, depends on target platform (> = big, < = little) | |
BYTE_ORDER = '<' | |
BLOCK_SIZE = 32 | |
# 0 - skip CGS | |
# 1 - assemble partial overlays | |
# 2 - assemble full CGs (not working) | |
ASSEMBLE_CGS = 1 | |
if len(sys.argv) == 1: | |
print("No files given.", file=sys.stderr) | |
exit(1) | |
args = sys.argv[1:] | |
if len(sys.argv) == 0: | |
args = glob.glob('*.mvl') | |
else: | |
for i, arg in enumerate(args): | |
if '*' in arg: | |
args = args[:i] + glob.glob(arg) + args[i+1:] | |
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 any(c.isdigit() for c in arg_basename): | |
if ASSEMBLE_CGS == 0: | |
continue | |
opf = 'output_cgs' | |
else: | |
opf = 'output_sprites' | |
if not os.path.exists(opf): | |
os.mkdir(opf) | |
os.chdir(opf) | |
if os.path.exists(arg_basename): | |
os.chdir('..') | |
continue | |
os.mkdir(arg_basename) | |
os.chdir(arg_basename) | |
print(arg_basename) | |
records_count = 0 | |
table_count = 0 | |
states = [] | |
records = [] | |
table = [] | |
table_start = None | |
with open(arg + '_.mvl', 'rb') as f: | |
# 4 chars: MVL1 | |
f.read(4) | |
# uint32: count of states | |
states_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
#print(states_count) | |
# uint32: input width | |
INPUT_WIDTH = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
#print(INPUT_WIDTH) | |
# 20 zeros | |
f.read(20) | |
# 64 characters | |
shit = str(f.read(64), encoding='ascii').strip('\0') | |
#print(shit) | |
for i in range(states_count): | |
# uint32: output width | |
SCREEN_WIDTH = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: output height | |
SCREEN_HEIGHT = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: idk | |
idk = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# 4 zeros | |
f.read(4) | |
# uint32: count of coordinates records | |
records_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: beginning of coordinates records | |
records_start = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: count of records to read | |
count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: beginning of records to read | |
start = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# 32 chars: state name | |
id = str(f.read(32), encoding='ascii').strip('\0') | |
if table_start == None: | |
table_start = start | |
start = (start - table_start) // 2 | |
table_count += count | |
if len(id) == 9: | |
states.append(State(id, count, start)) | |
elif len(id) == 11: | |
states[-1].overlays.setdefault(id[-2], []).append(Overlay(count, start)) | |
else: | |
print("weird!") | |
#print(id, count, start, sep='\t') | |
#print() | |
#print(output_width, output_height) | |
#print() | |
#print(records_count) | |
for i in range(records_count): | |
# float32: block position on the screen, X | |
x1 = round(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0])# + 1 | |
# float32: block position on the screen, Y | |
y1 = round(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0])# + 1 | |
# 4 zeros | |
f.read(4) | |
# float32: block position in the image, X | |
x2 = struct.unpack(BYTE_ORDER + 'f', f.read(4))[0]# - 1 | |
# float32: block position in the image, Y | |
y2 = struct.unpack(BYTE_ORDER + 'f', f.read(4))[0]# - 1 | |
#print(x1, y1, x2, y2) | |
records.append((x1, y1, x2, y2)) | |
#print() | |
#print(table_count) | |
for i in range(table_count): | |
(idx,) = struct.unpack(BYTE_ORDER + 'H', f.read(2)) | |
table.append(idx) | |
with Image.open(arg + '.png') as image: | |
# empty image | |
empty_im = Image.new('RGBA', (SCREEN_WIDTH, SCREEN_HEIGHT), (0, 0, 0, 0)) | |
for s in states: | |
print(s.id) | |
if ASSEMBLE_CGS == 2 and any(c.isdigit() for c in arg_basename): | |
cg_id = arg_basename[-4:] | |
if cg_id[0] == '_': | |
cg_id = cg_id[1:] | |
x = int(s.id[-2:]) | |
if x > 1: | |
cg_id += chr(ord('a') + x - 1) | |
s_im = Image.open('../../cgs/yng_' + cg_id + '.png') | |
else: | |
s_im = empty_im.copy() | |
for i in range(s.start, s.start + s.count, 6): | |
c = records[table[i]] | |
in_box = (round(c[2] * image.width) - 1, round(c[3] * image.height) - 1, round(c[2] * image.width) + BLOCK_SIZE - 1, round(c[3] * image.height) + BLOCK_SIZE - 1) | |
out_box = (SCREEN_WIDTH//2 + c[0] - 1, SCREEN_HEIGHT//2 + c[1] - 1) | |
region = image.crop(in_box) | |
s_im.paste(region, out_box) | |
for overlays in product(*map(enumerate, s.overlays.values())): | |
s_fname = s.id | |
for o, (num, overlay) in zip(s.overlays.keys(), overlays): | |
s_fname += o + str(num + 1) | |
for i in range(overlay.start, overlay.start + overlay.count, 6): | |
c = records[table[i]] | |
in_box = (round(c[2] * image.width) - 1, round(c[3] * image.height) - 1, round(c[2] * image.width) + BLOCK_SIZE - 1, round(c[3] * image.height) + BLOCK_SIZE - 1) | |
out_box = (SCREEN_WIDTH//2 + c[0] - 1, SCREEN_HEIGHT//2 + c[1] - 1) | |
region = image.crop(in_box) | |
s_im.paste(region, out_box) | |
s_im.save(s_fname + '.png') | |
s_im.close() | |
print() | |
os.chdir('..') | |
os.chdir('..') |
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
#!/usr/bin/env python3 | |
import sys | |
import glob | |
import os | |
import struct | |
import codecs | |
from PIL import Image, ImageTk | |
from itertools import product | |
class State: | |
def __init__(self, id, count, start): | |
self.id = id | |
self.count = count | |
self.start = start | |
self.overlays = {} | |
class Overlay: | |
def __init__(self, count, start): | |
self.count = count | |
self.start = start | |
class Sprite: | |
def __init__(self, path, width, height, states, records, table): | |
self.path = path | |
self.width = width | |
self.height = height | |
self.states = states | |
self.records = records | |
self.table = table | |
# endianness, depends on target platform (> = big, < = little) | |
BYTE_ORDER = '<' | |
BLOCK_SIZE = 32 | |
NAMES = {'ama': 'Amanda', 'ayu': 'Ayumi', 'chi': 'Child Yu-no', 'eri': 'Eriko', 'hoj': 'Houjou', 'kan': 'Kanna', 'kao': 'Kaori', 'kon': 'Imperial Guard', 'kou': 'Koudai', 'mio': 'Mio', 'mit': 'Mitsuki', 'osy': 'Marina (Female Guard)', 'rou': 'Ume', 'ryu': 'Ryuuzouji', 'saa': 'Some miner guy', 'sab': 'Some other miner guy', 'ser': 'Sayless', 'sin': 'God-Empress', 'syo': 'Quarry Chief', 'tak': 'Takuya', 'toy': 'Toyotomi', 'yuk': 'Yuuki', 'yun': 'Yu-no'} | |
OVERLAYS = {'L': 'Lips', 'E': 'Eyelids'} | |
args = sys.argv[1:] | |
if len(sys.argv) <= 1: | |
args = glob.glob('*.mvl') | |
else: | |
for i, arg in enumerate(args): | |
if '*' in arg: | |
args = args[:i] + glob.glob(arg) + args[i+1:] | |
sprites = {} | |
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) | |
# don't do CGs | |
if any(c.isdigit() for c in arg_basename): | |
continue | |
#print(arg_basename) | |
(_, name, pos) = arg_basename.split('_') | |
# skip Takuya's closeup | |
if pos[0] != 's': | |
continue | |
bodies = sprites.setdefault(name, []) | |
if len(bodies) == ord(pos[1]) - ord('a'): | |
bodies.append([]) | |
arms = bodies[-1] | |
records_count = 0 | |
table_count = 0 | |
states = [] | |
records = [] | |
table = [] | |
table_start = None | |
with open(arg + '_.mvl', 'rb') as f: | |
# 4 chars: MVL1 | |
f.read(4) | |
# uint32: count of states | |
states_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
#print(states_count) | |
# uint32: input width | |
input_width = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
#print(input_width) | |
# 20 zeros | |
f.read(20) | |
# 64 characters | |
shit = str(f.read(64), encoding='ascii').strip('\0') | |
#print(shit) | |
for i in range(states_count): | |
# uint32: output width | |
output_width = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: output height | |
output_height = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: idk | |
idk = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# 4 zeros | |
f.read(4) | |
# uint32: count of coordinates records | |
records_count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: beginning of coordinates records | |
records_start = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: count of records to read | |
count = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# uint32: beginning of records to read | |
start = struct.unpack(BYTE_ORDER + 'I', f.read(4))[0] | |
# 32 chars: state name | |
id = str(f.read(32), encoding='ascii').strip('\0') | |
if table_start == None: | |
table_start = start | |
start = (start - table_start) // 2 | |
table_count += count | |
if len(id) == 9: | |
states.append(State(id, count, start)) | |
elif len(id) == 11: | |
states[-1].overlays.setdefault(id[-2], []).append(Overlay(count, start)) | |
else: | |
print("weird!") | |
#print(id, count, start, sep='\t') | |
#print() | |
#print(output_width, output_height) | |
#print() | |
#print(records_count) | |
for i in range(records_count): | |
# float32: block position on the screen, X | |
x1 = round(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0])# + 1 | |
# float32: block position on the screen, Y | |
y1 = round(struct.unpack(BYTE_ORDER + 'f', f.read(4))[0])# + 1 | |
# 4 zeros | |
f.read(4) | |
# float32: block position in the image, X | |
x2 = struct.unpack(BYTE_ORDER + 'f', f.read(4))[0]# - 1 | |
# float32: block position in the image, Y | |
y2 = struct.unpack(BYTE_ORDER + 'f', f.read(4))[0]# - 1 | |
#print(x1, y1, x2, y2) | |
records.append((x1, y1, x2, y2)) | |
#print() | |
#print(map_count) | |
for i in range(table_count): | |
(idx,) = struct.unpack(BYTE_ORDER + 'H', f.read(2)) | |
table.append(idx) | |
arms.append(Sprite(arg + '.png', output_width, output_height, states, records, table)) | |
import tkinter as tk | |
from tkinter import ttk | |
class Application(ttk.Frame): | |
def __init__(self, master=None): | |
ttk.Frame.__init__(self, master) | |
self.grid() | |
self.createWidgets() | |
self.scales = {} | |
self.labels = {} | |
self.empty_im = None | |
self.compressed_im = None | |
self.base_im = None | |
self.display = None | |
self.sprite_id = None | |
def select_sprite(self, dummy_event=None): | |
if len(self.tree.focus()) != 8: | |
return | |
for scale in self.scales.values(): | |
scale.grid_forget() | |
for label in self.labels.values(): | |
label.grid_forget() | |
if self.sprite_id != self.tree.focus()[:6]: | |
self.sprite_id = self.tree.focus()[:6] | |
char = self.tree.focus()[:3] | |
outfit = ord(self.tree.focus()[4]) - ord('a') | |
pose = ord(self.tree.focus()[5]) - ord('a') | |
self.sprite = sprites[char][outfit][pose] | |
if self.empty_im: | |
self.empty_im.close() | |
if self.base_im: | |
self.base_im.close() | |
self.empty_im = Image.new('RGBA', (self.sprite.width, self.sprite.height), (0, 0, 0, 0)) | |
self.base_im = self.empty_im.copy() | |
if self.compressed_im != None: | |
self.compressed_im.close() | |
self.compressed_im = Image.open(self.sprite.path) | |
face = int(self.tree.focus()[6:8]) | |
s = self.state = self.sprite.states[face] | |
self.scales = {} | |
self.labels = {} | |
for i, (key, val) in enumerate(self.state.overlays.items()): | |
label = ttk.Label(self, text = OVERLAYS[key]) | |
label.grid(row=0, column=1+i) | |
self.labels[key] = label | |
scale = ttk.Scale(self, to=len(val)-1, command=self.redraw, length=200) | |
scale.grid(row=1, column=1+i) | |
self.scales[key] = scale | |
s_im = self.base_im | |
image = self.compressed_im | |
for i in range(s.start, s.start + s.count, 6): | |
c = self.sprite.records[self.sprite.table[i]] | |
in_box = (round(c[2] * image.width) - 1, round(c[3] * image.height) - 1, round(c[2] * image.width) + BLOCK_SIZE - 1, round(c[3] * image.height) + BLOCK_SIZE - 1) | |
out_box = (self.sprite.width//2 + c[0] - 1, self.sprite.height//2 + c[1] - 1) | |
region = image.crop(in_box) | |
s_im.paste(region, out_box) | |
self.redraw() | |
def redraw(self, dummy_event=None, *idk): | |
if not self.base_im: | |
return | |
for scale in self.scales.values(): | |
if scale.get() != round(scale.get()): | |
scale.set(round(scale.get())) | |
return | |
if self.display: | |
self.display.grid_forget() | |
s_im = self.base_im | |
image = self.compressed_im | |
for o, l in self.state.overlays.items(): | |
x = l[int(self.scales[o].get())] | |
for i in range(x.start, x.start + x.count, 6): | |
c = self.sprite.records[self.sprite.table[i]] | |
in_box = (round(c[2] * image.width) - 1, round(c[3] * image.height) - 1, round(c[2] * image.width) + BLOCK_SIZE - 1, round(c[3] * image.height) + BLOCK_SIZE - 1) | |
out_box = (self.sprite.width//2 + c[0] - 1, self.sprite.height//2 + c[1] - 1) | |
region = image.crop(in_box) | |
s_im.paste(region, out_box) | |
if self.height.get(): | |
render = ImageTk.PhotoImage(s_im.resize(((self.sprite.width * self.height.get()) // self.sprite.height, self.height.get()))) | |
else: | |
render = ImageTk.PhotoImage(s_im.copy()) | |
self.display = ttk.Label(self, image = render) | |
self.display.image = render | |
self.display.grid(row=2,column=1,columnspan=max(1, len(self.state.overlays))) | |
def createWidgets(self): | |
ttk.Label(self, text='Height').grid(row=0,column=0) | |
self.height = tk.IntVar() | |
self.height.trace('w', self.redraw) | |
e = ttk.Entry(self, textvariable=self.height) | |
e.grid(row=1,column=0) | |
ttk.Style().configure('Treeview', rowheight=40) | |
ttk.Style().configure('Horizontal.TScale', sliderthickness=40) | |
self.tree = ttk.Treeview(self, show='tree', selectmode='browse', height=20) | |
for char, outfit in sprites.items(): | |
self.tree.insert('', 'end', char, text = NAMES[char]) | |
for i, poses in enumerate(outfit): | |
id1 = char + '_' + chr(ord('a') + i) | |
self.tree.insert(char, 'end', id1, text = "Outfit " + str(i + 1)) | |
for j, sprite in enumerate(poses): | |
id2 = id1 + chr(ord('a') + j) | |
self.tree.insert(id1, 'end', id2, text = "Pose " + str(j + 1)) | |
for k, face in enumerate(sprite.states): | |
id3 = id2 + '{:02}'.format(k) | |
self.tree.insert(id2, 'end', id3, text = "Face " + str(k + 1)) | |
self.tree.column('#0', width=300) | |
self.tree.bind('<<TreeviewSelect>>', self.select_sprite) | |
self.tree.grid(row=2,column=0) | |
app = Application() | |
app.master.title('YUNO Sprite Viewer') | |
app.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment