442 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			442 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| -- ASEPRITE tileset and tilemap C exporter for use with GBDK  --  version 0.3
 | |
|     --SEPTEMBER/2022--
 | |
| 
 | |
| 
 | |
| --DISCLAIMER--
 | |
| -- I'm an amateur programer, so the code may not be as optimal as it could be
 | |
| -- also, some notes may seem over explained, but they are for me to understand and remember what everything does.
 | |
| 
 | |
| --helpeful resources :
 | |
|     --https://eldred.fr/gb-asm-tutorial/part1/tiles.html
 | |
|     --https://youtu.be/txkHN6izK2Y?t=344
 | |
| 
 | |
|     -- Big Thanks to Aseprite community, specifically:
 | |
|     -- Jeremy Behreandt, whose tutorial helped get started on the script
 | |
|     -- 
 | |
|     -- boombuler, for his version of the plugin, (found here : https://github.com/boombuler/aseprite-gbexport)
 | |
|     -- that helped a lot during debugging, and figuring out how to output the tiles
 | |
|     
 | |
|     -- ONLY WORKS WITH A SINGLE TILEMAP LAYER
 | |
|     
 | |
| 
 | |
| -- revised by shoofle around new years 2024/2025. this should export just the selection and just the currently active layer.
 | |
| -- select an area to export just that.
 | |
| -- exports to assembly as well.
 | |
| -- 
 | |
| 
 | |
| local sprt = app.activeSprite
 | |
| local layer = app.activeLayer
 | |
| local selection = sprt.selection
 | |
| local tile_layers ={}
 | |
| local n_layer = 0
 | |
| local file_format = ""
 | |
|     
 | |
| local filepath = sprt.filename
 | |
|     
 | |
| local plt = sprt.palettes[1] -- DEFINES THE PALETTE
 | |
| 
 | |
| local tile_amount = 0
 | |
| local tile_offset = 0
 | |
| 
 | |
| local folder = string.find(filepath,"[^/]+$") - 1
 | |
| filepath = string.sub(filepath,1,folder)
 | |
| 
 | |
| local tile_name = ""
 | |
| local map_name = ""
 | |
| local do_set = false -- EXPORT TILESET
 | |
| local do_map = false -- EXPORT TILEMAP
 | |
| 
 | |
| local w = 0 -- MAP WIDTH
 | |
| local h = 0 -- MAP HEIGHT
 | |
| 
 | |
| -- INITIAL CHECKS FOR VALID FILE -- 
 | |
| 
 | |
| if sprt == nil then --CHECKS IF THERE'S AN IMAGE LOADED
 | |
|     app.alert{
 | |
|         title = "MAJOR ERROR",
 | |
|         text = "THERE'S NO FILE OPEN",
 | |
|         buttons = "Oh crap"
 | |
|     }
 | |
|     return
 | |
| end
 | |
| 
 | |
| for i,current_layer in ipairs(sprt.layers) do
 | |
|     if current_layer.isTilemap then
 | |
|         tile_layers[n_layer] = current_layer
 | |
|         n_layer = n_layer+1
 | |
|     end
 | |
| end
 | |
| 
 | |
| if n_layer == 0 then
 | |
|     app.alert{
 | |
|         title = "ERROR",
 | |
|         text = "There is no Tilemap Layer"
 | |
|     }
 | |
|     return
 | |
| end
 | |
| 
 | |
| if ColorMode.TILEMAP == nil then --CHECKS FOR TILEMAP
 | |
|     app.alert{
 | |
|         title = "ERROR",
 | |
|         text = "This file does not make use of tilemaps"
 | |
|     }
 | |
|     return
 | |
| end
 | |
| 
 | |
| 
 | |
| if sprt.height % 8 ~= 0 or sprt.width%8 ~= 0 then --CHECKS FOR IMAGE DIMENSIONS, GAMEBOY USES 8X8 TILES, FOR THE IMAGE MUST BE A MULTIPLE
 | |
|     app.alert {
 | |
|         title = "ERROR",
 | |
|         text = "Canvas width or height is not multiple of 8.",
 | |
|         buttons = "OK"
 | |
|     }
 | |
|     return
 | |
| end
 | |
| 
 | |
| if (layer.tileset.grid.tileSize.width ~= 8 or layer.tileset.grid.tileSize.height ~= 8) then
 | |
|     app.alert {
 | |
|         title = "ERROR",
 | |
|         text = "Tile Size is not 8x8 px",
 | |
|         buttons = "OK"
 | |
|     }
 | |
|     return
 | |
| end
 | |
| 
 | |
| if sprt.colorMode ~= ColorMode.INDEXED then --CHECKS IF THE COLOR MODE IS SET TO INDEXED
 | |
|     app.alert{
 | |
|         title = "ERROR",
 | |
|         text = {" Color Mode must be", "INDEXED", "and have only 4 colors"},
 | |
|         buttons = "Oh, my bad"
 | |
|     }
 | |
|     return
 | |
| elseif #plt ~= 4 then  -- IF IT IS INDEXED, CHECKS IF IT DOES HAVE 4 COLORS 
 | |
|     app.alert{
 | |
|         title = "ERROR",
 | |
|         text = "Number of colors MUST BE 4",
 | |
|         buttont  = "Oops"
 | |
|     }
 | |
|     return
 | |
| end
 | |
| 
 | |
| 
 | |
| --INITIAL CHECKS DONE--
 | |
| 
 | |
| --USER INPUT--
 | |
| 
 | |
| local dlg = Dialog {title = "GB TILE EXPORTER"}
 | |
| 
 | |
| dlg:label{
 | |
|     id = "label-01",
 | |
|     text = "Pick a location to save the file."
 | |
| }
 | |
| dlg:newrow()
 | |
| 
 | |
| dlg:label{
 | |
|     id = "label-02",
 | |
|     text = "Only the folder will be used."
 | |
| }
 | |
| dlg:file{
 | |
|     id = "filepath",
 | |
|     label = "Save Location",
 | |
|     save = true,
 | |
| } 
 | |
| 
 | |
| local tileset_name_prompt = "tileset"
 | |
| if layer.tileset.name ~= "" then
 | |
|     tileset_name_prompt = layer.tileset.name:gsub(" ", "")
 | |
|     tileset_name_prompt = "." .. tileset_name_prompt
 | |
| end
 | |
| dlg:entry{
 | |
|     id="tilename",
 | |
|     label = "Tileset Name",
 | |
|     text = tileset_name_prompt
 | |
| }
 | |
| 
 | |
| local tilemap_name_prompt = "tilemap"
 | |
| if layer.name ~= "" then
 | |
|     tilemap_name_prompt = layer.name:gsub(" ", "") 
 | |
|     tilemap_name_prompt = "." .. tilemap_name_prompt
 | |
| end
 | |
| dlg:entry{
 | |
|     id = "mapname",
 | |
|     label = "Map Name",
 | |
|     text = tilemap_name_prompt
 | |
| }
 | |
| 
 | |
| dlg:entry {
 | |
|     id = "tile_offset",
 | |
|     label = "Tile Index Start",
 | |
|     text = "0"
 | |
| }
 | |
| 
 | |
| dlg:combobox{
 | |
|     id = "fileformat",
 | |
|     label = "File Format",
 | |
|     option = "ASM",
 | |
|     options = {"C", "ASM"} -- FOR FUTURE SUPPORT OF DIFFERENTE TYPES, LIKE ASM MAYBE
 | |
| }
 | |
| dlg:newrow()
 | |
| 
 | |
| dlg:check{
 | |
|     id = "checkTileset",
 | |
|     text = "Export Tileset",
 | |
|     selected = true,
 | |
|     -- bounds = Rectangle()
 | |
| }
 | |
| dlg:check{
 | |
|     id = "checkTilemap",
 | |
|     text = "Export Tilemap",
 | |
|     selected = true
 | |
| }
 | |
| -- NEEDS FILE PATH TO EXPORT
 | |
| -- NAME FOR TILESET
 | |
| -- NAME FOR TILEMAP
 | |
| -- CHECK BOX FOR TILESET(EXPORT TILESET)
 | |
| -- CHECK BOX FOR TILEMAP(EXPORT TILEMAP)
 | |
| 
 | |
| dlg:button{
 | |
|     id="confirm",
 | |
|     text="OK"
 | |
| }
 | |
| dlg:button{
 | |
|     id="cancel",
 | |
|     text="Cancel"
 | |
| }
 | |
| 
 | |
| 
 | |
| dlg:show{wait=true}
 | |
| 
 | |
| --FUNCTIONS ARE DECLARED HERE-- 
 | |
| 
 | |
| local dlg_data = dlg.data -- SAVES USER INPUT
 | |
| 
 | |
| local function tile_to_hex(tile) -- 
 | |
|     
 | |
|     -- ****HOW TO GAMEBOY WORKS WITH 2BITS PER PIXEL (2BPP)****--
 | |
|     
 | |
|     -- EACH PIXEL IS A SET OF TWO DIGITS (BITS) SO WE TAKE A ROW AS A FULL BYTE
 | |
|     -- SEPARATES THE NUMBER(BINARY) INTO PAIRS OF DIGITS
 | |
|     -- THE PAIRS ARE THEM SEPARATED INTO  "HIGH BITE" (LEFT) AND "LOW BITE" (RIGHT)
 | |
|     -- THEM YOU JOIN THE HIGHS AND THE LOWS, AND END UP WITH TWO BYTES (8 DIGITS EACH)
 | |
|     -- THEN YOU PUT THE LOW BYTES FIRST AND THE HIGHS SECOND
 | |
| 
 | |
|     local hex = {}
 | |
|     local range_x = tile.width-1
 | |
|     local range_y = tile.height-1
 | |
|     for y = 0, range_y do --LOOPS Y AXIS
 | |
|         local lo_bit = 0;  --resets the low bit per each y value (for each row)
 | |
|         local hi_bit = 0;  -- resets the high bit per each y value (for each row)
 | |
|         for x = 0, range_x do -- LOOPS X AXIS
 | |
|             local pixel = tile:getPixel(x,y)
 | |
|             if (pixel & 1) ~= 0 then  -- 1 IN BINARY = 01 
 | |
|                 lo_bit = lo_bit | (1 << range_x-x) -- THE OPERATOR (1<< n-0) would be invalid, so we add (lo_bit |) 
 | |
|             end
 | |
|             
 | |
|             if (pixel & 2) ~= 0 then -- 2 IN BINARY == 10
 | |
|                 hi_bit = hi_bit | (1 << range_x-x)
 | |
|             end
 | |
|             -- WAS USING AND INSTEAD OF & AND APARENTLY THAT WAS MESSING UP THE CODE, THANKS AGAIN TO boombuler FOR HIS CODE (LIFE SAVING)
 | |
|         end
 | |
|         table.insert(hex, string.format("%02x", lo_bit))
 | |
|         table.insert(hex, string.format("%02x", hi_bit))
 | |
|     end
 | |
|     
 | |
|     return hex -- returns a list of hex values for this tile ;
 | |
|     
 | |
| end
 | |
| 
 | |
| local function generate_tileset(tileset) 
 | |
|     local t = {}
 | |
|     local grid = tileset.grid
 | |
|     local size = grid.tileSize
 | |
|     tile_amount = #tileset
 | |
| 
 | |
|     for i = 0, #tileset-1 do
 | |
|         local tile = tileset:getTile(i)
 | |
|         local hex = tile_to_hex(tile)
 | |
|         -- t[i] = tile_to_hex(tile)
 | |
|         -- print(hex)
 | |
|         table.insert(t,hex)
 | |
|         
 | |
|     end
 | |
| 
 | |
|     return t
 | |
| 
 | |
| end
 | |
| 
 | |
| local function generate_tilemap()
 | |
|     local sprite = app.sprite
 | |
|     local cel = app.cel
 | |
|     local image = cel.image
 | |
|     local layer = app.layer
 | |
|     local grid_size = layer.tileset.grid.tileSize
 | |
| 
 | |
|     --print("cel: " .. cel.bounds.x .. ',' .. cel.bounds.y .. " " .. cel.bounds.width .. "x" .. cel.bounds.height)
 | |
|     --print("sprite: " ..sprite.width .. 'x' .. sprite.height)
 | |
|     --print(image:getPixel(100,100))
 | |
| 
 | |
|     local xi = 0
 | |
|     local yi = 0
 | |
|     local w = sprite.width
 | |
|     local h = sprite.height
 | |
| 
 | |
|     if (not selection.isEmpty) then
 | |
|         xi = selection.bounds.origin.x
 | |
|         yi = selection.bounds.origin.y
 | |
|         w = selection.bounds.width
 | |
|         h = selection.bounds.height
 | |
|     end
 | |
| 
 | |
|     local map = {}
 | |
|     for y = (yi / grid_size.height) - (cel.bounds.y / grid_size.height) ,
 | |
|             (yi / grid_size.height) - (cel.bounds.y / grid_size.height) + (h / grid_size.height)-1 do
 | |
|         local line = {}
 | |
|         for x = (xi / grid_size.width) - (cel.bounds.x / grid_size.width), 
 | |
|                 (xi / grid_size.width) - (cel.bounds.x / grid_size.width) + (w / grid_size.width)-1 do
 | |
|             local value = image:getPixel(x, y)
 | |
|             if value == 0xffffffff then
 | |
|                 value = 0
 | |
|             end
 | |
|             table.insert(line, value)
 | |
|         end
 | |
|         table.insert(map, line)
 | |
|     end
 | |
| 
 | |
|     return map
 | |
| 
 | |
| 
 | |
| end
 | |
| 
 | |
| 
 | |
| local function save_to_file(name, contents)
 | |
|     --TODO: FIX IMPLEMENTATION OF THE SAVE TO FILE FUNTION TO INCLUDE DESIRED FOLDER
 | |
|     local file = io.open(name,"w")
 | |
|     file:write(contents)
 | |
|     file:close()
 | |
| end
 | |
| 
 | |
| 
 | |
| local function parse_input()
 | |
|     tile_name = dlg_data.tilename
 | |
|     map_name = dlg_data.mapname
 | |
|     if (dlg_data.filepath ~= "") then
 | |
|         filepath = dlg_data.filepath
 | |
|         folder = string.find(filepath,"[^/]+$") -1
 | |
|         filepath = string.sub(filepath,1,folder)
 | |
|         
 | |
|         print(filepath)
 | |
|     end
 | |
|     do_set = dlg_data.checkTileset
 | |
|     do_map = dlg_data.checkTilemap
 | |
|     file_format = dlg_data.fileformat
 | |
|     tile_offset = tonumber(dlg_data.tile_offset)
 | |
| 
 | |
|     -- print(exp_tileset)
 | |
|     -- print(exp_tilemap)
 | |
| end
 | |
| 
 | |
| local function export_c(tab,map)
 | |
|     local c_file = ""
 | |
|     local h_file = ""
 | |
|     c_file = c_file.. "// GENERATED USING ASEPRITE GB EXPORTER BY GABRIEL REIS// \n \n \n" -- header i suppose
 | |
|     h_file = c_file
 | |
| 
 | |
|     if do_map then
 | |
|         h_file = h_file.. "#define "..map_name.."_width "..tostring(#map[1]).."\n"
 | |
|         h_file = h_file.. "#define "..map_name.."_height "..tostring(#map).."\n"
 | |
|     end
 | |
| 
 | |
|     if do_set then   --CHECKS IF TILESET IS TO BE EXPORTED
 | |
|         c_file = c_file.. "const unsigned char " .. tile_name .. "[] = {\n"
 | |
| 
 | |
|         h_file = h_file .. "\n#define ".. tile_name.."_size "..tostring(#tab).."\n\n\n"
 | |
| 
 | |
|         h_file = h_file.. "extern const unsigned char ".. tile_name .."[]; \n\n"
 | |
|         
 | |
|         for i, tileset in ipairs(tab) do                    -- TILESET IN TILESETS
 | |
|             for j, tiles in ipairs(tileset) do              -- TILES IN TILESET
 | |
|                 c_file = c_file .. tiles
 | |
|             end
 | |
|         end
 | |
|         c_file = c_file .. "}; \n \n \n"
 | |
|         h_file = h_file .. "\n \n \n"
 | |
|     end
 | |
| 
 | |
|     if do_map then -- CHECKS IF MAP IS TO BE EXPORTED
 | |
| 
 | |
|         c_file = c_file .. "const unsigned char " .. map_name .. "[] = {\n" .. map .. "};"
 | |
|         h_file = h_file .. "extern const unsigned char " .. map_name .. "[];"
 | |
|     end
 | |
| 
 | |
|     return c_file, h_file
 | |
| end
 | |
| 
 | |
| local function export_asm(tab,map)
 | |
|     local out_contents = ""
 | |
|     out_contents= out_contents.. "\t; original export script by gabriel reis, modified by shoofle\n \n \n" -- header i suppose
 | |
| 
 | |
|     if do_set then   --CHECKS IF TILESET IS TO BE EXPORTED
 | |
|         out_contents = out_contents.. tile_name .. ":\n"
 | |
| 
 | |
|         for j, hexes in ipairs(tab) do              -- TILES IN TILESET
 | |
|             local hexen = {}
 | |
|             for x, h in ipairs(hexes) do
 | |
|                 table.insert(hexen, "$" .. h)
 | |
|             end
 | |
|             out_contents = out_contents .. "\tdb " .. table.concat(hexen, ",") .. "\n"
 | |
|         end
 | |
|         out_contents = out_contents .. tile_name .. "End:\n"
 | |
| 
 | |
|         out_contents = out_contents .. "\n"
 | |
|     end
 | |
| 
 | |
|     if do_map then -- CHECKS IF MAP IS TO BE EXPORTED
 | |
|         out_contents = out_contents .. map_name .. ": ; tiles start at " .. tile_offset .. "\n"
 | |
|         for i, line in ipairs(map) do
 | |
|             local linen = {}
 | |
|             for x, mark in ipairs(line) do
 | |
|                 table.insert(linen, string.format("$%02x", mark+tile_offset))
 | |
|             end
 | |
|             out_contents = out_contents .. "\tdb " .. table.concat(linen, ", ") .. "\n"
 | |
|         end
 | |
|         out_contents = out_contents .. map_name .. "End:\n"
 | |
|     end
 | |
| 
 | |
|     return out_contents
 | |
| end
 | |
| 
 | |
| 
 | |
| local function do_code()
 | |
|     local tiles = generate_tileset(layer.tileset) -- GENERATES TILESET
 | |
|     local map = generate_tilemap()  -- GENERATES TILEMAP
 | |
| 
 | |
|     local file_name = filepath .. tile_name
 | |
| 
 | |
|     if string.find(sprt.filename, ".aseprite$") ~= nil then
 | |
|         file_name = string.gsub(sprt.filename, ".aseprite$", "")
 | |
|     end
 | |
| 
 | |
|     if (file_format == "C") then
 | |
|         local c_file, h_file = export_c(tiles, map)
 | |
|         save_to_file(file_name ..".c", c_file)
 | |
|         save_to_file(file_name..".h", h_file)
 | |
| 
 | |
|         print(h_file.."\n")
 | |
|         print(c_file)
 | |
|     end
 | |
|     if (file_format == "ASM") then
 | |
|         local asm_file = export_asm(tiles, map)
 | |
|         save_to_file(file_name..".asm", asm_file)
 | |
|     end
 | |
|     
 | |
| end
 | |
| --CODE EXECUTION--
 | |
| 
 | |
| if dlg_data.confirm then
 | |
|     parse_input()
 | |
|     do_code()  --CODE GOES IN THERE TO STOP EXECUTION IF USER DIDN'T CLICK IN OK
 | |
| end
 | |
| if dlg_data.cancel then
 | |
|     return
 | |
| end
 |