Skip to content

Instantly share code, notes, and snippets.

@jneen
Created April 26, 2025 06:28
Show Gist options
  • Save jneen/f0ce7111190f016503c2b18dd93c26ff to your computer and use it in GitHub Desktop.
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
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