Created
April 26, 2025 06:28
-
-
Save jneen/f0ce7111190f016503c2b18dd93c26ff to your computer and use it in GitHub Desktop.
script to convert aseprite files to GFX bin files for use in SMW
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
require 'pry' | |
require 'chunky_png' | |
require 'zlib' | |
PLAYERSPRITE_DIR = File.expand_path('../resources/patches/player_sprite', __dir__) | |
class AseParser | |
def self.parse_file(fname) | |
File.open(fname, 'rb', encoding: 'ASCII-8BIT') do |file| | |
new(file).parse | |
end | |
end | |
def initialize(file) | |
@file = file | |
end | |
class Layer | |
attr_reader :height, :width, :pixels | |
def initialize(height, width, pixels) | |
@height = height | |
@width = width | |
@pixels = pixels | |
end | |
def pix(row, col) | |
@pixels[row * @width + col] | |
end | |
end | |
def layer(name) | |
layer = @layers.find { |layer| layer[:name] == name } or return nil | |
Layer.new(@height, @width, layer[:pixels]) | |
end | |
def parse | |
@fsize = read_dword | |
@magic = read_word | |
raise 'oh no' unless @magic == 0xA5E0 | |
@frame_count = read_word | |
raise 'oh no' unless @frame_count == 1 | |
@width = read_word | |
@height = read_word | |
@depth = read_word | |
raise 'oh no' unless @depth == 8 # (indexed) | |
@flags = read_dword | |
@speed = read_word | |
read_dword | |
read_dword | |
@transparent_idx = read_byte | |
@file.read(3) | |
@col_count = read_word | |
@pxwidth = read_byte | |
@pxheight = read_byte | |
@xpos = read_short | |
@ypos = read_short | |
@grid_width = read_word | |
@grid_height = read_word | |
@file.read(84) | |
read_frame | |
self | |
end | |
def read_frame | |
frame_size = read_dword | |
magic = read_word | |
raise 'oh no' unless magic == 0xF1FA | |
old_chunk_count = read_word | |
duration = read_word | |
@file.read(2) | |
chunk_count = read_dword | |
chunk_count = old_chunk_count if chunk_count == 0 | |
@layers = [] | |
chunks = (0...chunk_count).map do | |
read_chunk | |
end | |
chunks | |
end | |
def read_chunk | |
size = read_dword | |
type = read_word | |
fin = @file.pos - 6 + size | |
case type | |
when 0x2004 # Layer chunk | |
@layers << read_layer | |
when 0x2005 # Cel chunk | |
read_cel(fin) | |
else | |
puts "(skipping chunk #{sprintf("%04x", type)})" | |
end | |
@file.seek(fin) | |
end | |
def read_cel(fin) | |
layer_index = read_word | |
raise 'oh no' unless @layers[layer_index] | |
xpos = read_short | |
ypos = read_short | |
opacity = read_byte | |
cel_type = read_word | |
raise 'oh no' unless cel_type == 2 # compressed image | |
zindex = read_short | |
@file.read(5) | |
# cel type 2 | |
width = read_word | |
height = read_word | |
compressed = @file.read(fin - @file.pos) | |
pixels = Zlib::Inflate.inflate(compressed).unpack('C*') | |
@layers[layer_index][:pixels] = pixels | |
end | |
def read_layer | |
flags = read_word | |
type = read_word | |
child_level = read_word | |
read_word | |
read_word | |
blend_mode = read_word | |
opacity = read_byte | |
@file.read(3) | |
name = read_string | |
{ | |
flags: flags, | |
type: type, | |
child_level: child_level, | |
blend_mode: blend_mode, | |
opacity: opacity, | |
name: name | |
} | |
end | |
def read_byte | |
@file.read(1).unpack1('C') | |
end | |
def read_word | |
@file.read(2).unpack1('S<') | |
end | |
def read_short | |
@file.read(2).unpack1('s<') | |
end | |
def read_dword | |
@file.read(4).unpack1('l<') | |
end | |
def read_long | |
@file.read(4).unpack1('L<') | |
end | |
def read_string | |
size = read_word | |
@file.read(size) | |
end | |
def read_point | |
x = read_long | |
y = read_long | |
[x, y] | |
end | |
def read_size | |
read_point | |
end | |
def read_rect | |
x, y = read_point | |
w, h = read_size | |
[x, y, w, h] | |
end | |
def read_indexed_pixel | |
read_byte | |
end | |
end | |
module GFX | |
class Palette | |
def initialize(colors) | |
@colors = colors | |
binding.pry unless colors.sort.uniq.size == 16 | |
end | |
def self.parse(text) | |
pal = [] | |
text.scan /(\d+) (\d+) (\d+)\n/ do | |
next pal << 0 if [$1, $2, $3].uniq == ['0'] | |
pal << sprintf("%02x%02x%02xff", $1, $2, $3).to_i(16) | |
end | |
raise 'oh no' unless pal.sort.uniq.size == 16 | |
new(pal) | |
end | |
def get(color) | |
idx = @colors.index(color) | |
binding.pry if idx.nil? | |
idx | |
end | |
end | |
class Renderer | |
def initialize(layer) | |
@layer = layer | |
raise 'oh no' unless @layer.width % 8 == 0 | |
raise 'oh no' unless @layer.height % 8 == 0 | |
end | |
def render | |
out = [] | |
each_8x8 do |row8, col8| | |
[[0, 1], [2, 3]].each do |group| | |
(0...8).each do |row| | |
group.each do |plane| | |
mask = (1 << plane) | |
num = 0 | |
(0...8).each do |col| | |
num <<= 1 | |
num |= ((pix(row8 + row, col8 + col) & mask) >> plane) | |
end | |
out << num | |
end | |
end | |
end | |
end | |
out.pack('c*') | |
end | |
private | |
def pix(row, col) | |
@layer.pix(row, col) | |
end | |
def each_8x8(&b) | |
([email protected]/8).each do |row8| | |
([email protected]/8).each do |col8| | |
yield row8 * 8, col8 * 8 | |
end | |
end | |
end | |
end | |
end | |
def cli(argv) | |
mode = argv.shift | |
case mode | |
when 'render' | |
infile = argv.shift | |
outfile = argv.shift | |
puts "parsing aseprite file #{infile}..." | |
ase = AseParser.parse_file(infile) | |
layer = ase.layer('OUTPUT') | |
puts "done" | |
bytes = GFX::Renderer.new(layer).render | |
print "writing gfx bin to #{outfile}..." | |
File.binwrite(outfile, bytes) | |
puts "done" | |
else | |
binding.pry | |
end | |
end | |
cli(ARGV.to_a) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment