434 lines
12 KiB
Lua
434 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(" ", "")
|
||
|
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(" ", "")
|
||
|
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"
|
||
|
|
||
|
out_contents = out_contents.."\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 .. "\n \n \n"
|
||
|
end
|
||
|
|
||
|
if do_map then -- CHECKS IF MAP IS TO BE EXPORTED
|
||
|
out_contents = out_contents .. map_name .. ":\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
|
||
|
end
|
||
|
|
||
|
return out_contents
|
||
|
end
|
||
|
|
||
|
|
||
|
local function do_code()
|
||
|
local tiles = generate_tileset(layer.tileset) -- GENERATES TILESET
|
||
|
local map = generate_tilemap() -- GENERATES TILEMAP
|
||
|
|
||
|
if (file_format == "C") then
|
||
|
local c_file, h_file = export_c(tiles, map)
|
||
|
save_to_file(filepath..tile_name..".c", c_file)
|
||
|
save_to_file(filepath..tile_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(filepath..tile_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
|