#!/usr/bin/env ruby require 'rmagick' require 'yaml' require 'erb' data = YAML.load(File.read('spritesheet.yml')) class Sprite def initialize(image:, sx:, sy:, width:, height:) @image = image @sx = sx @sy = sy @width = width @height = height end def pixel_data @height.times.each_with_object([]) do |y, obj| obj << @width.times.map do |x| color = @image.pixel_color(x + @sx, y + @sy) colormap.find_index { |c| c == color } end end end private def colormap @colormap ||= @image.colors.times.map do |i| Magick::Pixel.from_color(@image.colormap(i)) end end end MovBytePtr = Data.define(:offset, :data) ASM_TEMPLATE = <<~ASM <% sprites.each do |sprite| %> PUBLIC <%= sprite.exported_name %><% end %> PUBLIC _palette .386 .model flat,c .DATA _palette: <%= palette.flat_map { |color| color.to_asm_struct }.join("\n") %> .CODE <% sprites.each do |sprite| %> <%= sprite.exported_name %>: push ebp mov ebp, esp <% sprite.bytes.each do |byte| %> mov BYTE PTR [eax + <%= byte.offset %>], <%= byte.data %><% end %> pop ebp ret <% end %> end ASM H_TEMPLATE = <<~H #ifndef __SPRITES_H__ #define __SPRITES_H__ #include "types.h" #include "system/vga.h" #define PALETTE_COLOR_COUNT (<%= palette.length %>) extern struct VGAColor palette[<%= palette.length %>]; <% sprites.each do |sprite| %> extern void <%= sprite.function_name %>(byte *); <% sprite.constants.each do |constant, value| %> #define <%= constant %> (<%= value %>) <% end %> <% end %> #endif H class AssemblerSprite def initialize( name:, pixel_data:, screen_width:, transparent_color:, offset_x:, offset_y:, prefix: ) @name = name @pixel_data = pixel_data @screen_width = screen_width @transparent_color = transparent_color @offset_x = offset_x @offset_y = offset_y @prefix = prefix end def exported_name "#{function_name}_" end def function_name "#{@prefix}_#{@name}" end def constants [ ['WIDTH', @pixel_data[0].length], ['HEIGHT', @pixel_data.length], ['OFFSET_X', @offset_x], ['OFFSET_Y', @offset_y] ].map do |suffix, value| ["#{function_name.upcase}_#{suffix}", value] end end def bytes return @bytes if defined?(@bytes) bytes = [] @pixel_data.each_with_index do |row, y| row.each_with_index do |column, x| next if @pixel_data[y][x] == @transparent_color bytes << MovBytePtr.new( data: @pixel_data[y][x], offset: x - @offset_x + (y - @offset_y) * @screen_width ) end end bytes end end Screen = Data.define(:width, :transparent_color) SpriteData = Data.define(:image, :name, :position, :dimensions, :offset, :prefix) screen = Screen.new( width: data["screen"]["width"], transparent_color: data["screen"]["transparentColor"] ) sprite_data = [] palette = nil VGAColor = Data.define(:red, :green, :blue) do def to_asm_struct [ " BYTE #{red}", " BYTE #{green}", " BYTE #{blue}" ] end end data["files"].each do |spritesheet| image = Magick::Image.read(spritesheet["file"]).first unless palette palette = image.colors.times.map do |i| pixel = Magick::Pixel.from_color(image.colormap(i)) VGAColor.new( red: pixel.red >> 10, green: pixel.green >> 10, blue: pixel.blue >> 10 ) end end spritesheet["sprites"].each do |name, details| sprite_data << SpriteData.new( image:, name:, prefix: spritesheet["prefix"], position: details["position"], dimensions: details["dimensions"], offset: details["offset"] ) end end sprites = sprite_data.map do |sd| sprite = Sprite.new( image: sd.image, sx: sd.position[0], sy: sd.position[1], width: sd.dimensions[0], height: sd.dimensions[1], ) AssemblerSprite.new( name: sd.name, prefix: sd.prefix, pixel_data: sprite.pixel_data, screen_width: screen.width, transparent_color: screen.transparent_color, offset_x: sd.offset[0], offset_y: sd.offset[1], ) end File.open('sprites.asm', 'w') do |fh| fh.puts ERB.new(ASM_TEMPLATE).result_with_hash(sprites:) end File.open('sprites.h', 'w') do |fh| fh.puts ERB.new(H_TEMPLATE).result_with_hash(sprites:, palette:) end puts "sprites.{asm,h} written"