From: Ben Sherratt Date: Thu, 18 Dec 2025 23:48:01 +0000 (+0000) Subject: Initial commit X-Git-Url: https://git.bts.cx/aseprite-tools.git/commitdiff_plain/14c3f1ea993c163f0524a373af47ae802825fba5?ds=inline Initial commit --- 14c3f1ea993c163f0524a373af47ae802825fba5 diff --git a/export-metrics.lua b/export-metrics.lua new file mode 100644 index 0000000..f06294c --- /dev/null +++ b/export-metrics.lua @@ -0,0 +1,110 @@ +local pluginKey = "bts/metrics" + +local sprite = app.sprite + +local name = app.fs.fileTitle(sprite.filename) + +local previousExportFilename = sprite.properties(pluginKey).previousExportFilename + +if previousExportFilename == nil then + local defaultPath = app.fs.filePath(sprite.filename) + local defaultFilename = name..".lua" + previousExportFilename = app.fs.joinPath(defaultPath, defaultFilename) +end + +local dlg = Dialog() +dlg:file{ id="metrics_filename", label="Metrics output:", filename=previousExportFilename, filetypes=".lua", save=true } +dlg:button{ id="confirm", text="Confirm" } +dlg:button{ id="cancel", text="Cancel" } +dlg:show() + +local data = dlg.data +if data.confirm then + name = app.fs.fileTitle(data.metrics_filename) + + local basePath = app.fs.filePath(data.metrics_filename) + local imagePath = app.fs.joinPath(basePath, name) + + local layerLookup = {} + local originalLayerVisible = {} + + for i, layer in ipairs(sprite.layers) do + if layer.parent == layer.sprite then + originalLayerVisible[layer.name] = layer.isVisible + layer.isVisible = false + + layerLookup[layer.name] = layer + end + end + + local imageDetails = {} + + for i, slice in ipairs(sprite.slices) do + local id = slice.name + + if layerLookup[id] then + local layer = layerLookup[id] + local filename = app.fs.joinPath(imagePath, id..".png") + + layer.isVisible = true + app.command.SaveFileCopyAs{ ui=false, filename=filename, slice=id } + layer.isVisible = false + + table.insert(imageDetails, id) + end + end + + local metricsDetails = {} + + for i, slice in ipairs(sprite.slices) do + local id = slice.name + local sliceMetrics = {} + + sliceMetrics["origin"] = { slice.bounds.x, slice.bounds.y} + sliceMetrics["size"] = { slice.bounds.width, slice.bounds.height} + sliceMetrics["center"] = { (slice.bounds.x + slice.bounds.width / 2), (slice.bounds.y + slice.bounds.height / 2)} + + metricsDetails[id] = sliceMetrics + end + + --[[if false then + local metricsFilename = app.fs.joinPath(metricsPath, name..".ui.metrics.json") + local metricsFile = io.open(metricsFilename, "w") + local metricsJson = json.encode(metricsDetails) + metricsFile:write(metricsJson) + io.close(metricsFile) + end]] + + if true then + --local metricsFilename = app.fs.joinPath(metricsPath, data.metrics_filename) + local metricsFile = io.open(data.metrics_filename, "w") + + metricsFile:write(name.."ImagesTable = {\n") + for k,v in ipairs(imageDetails) do + metricsFile:write("\t\"" .. v .. "\",\n") + end + metricsFile:write("}\n") + metricsFile:write("\n") + + metricsFile:write(name.."MetricsTable = {\n") + for k,v in pairs(metricsDetails) do + metricsFile:write("\n") + metricsFile:write("\t[\"" .. k .. ".origin\"] = { x=" .. v["origin"][1] .. ", y=" .. v["origin"][2] .. " },\n") + metricsFile:write("\t[\"" .. k .. ".size\"] = { w=" .. v["size"][1] .. ", h=" .. v["size"][2] .. " },\n") + metricsFile:write("\t[\"" .. k .. ".center\"] = { x=" .. v["center"][1] .. ", y=" .. v["center"][2] .. " },\n") + end + metricsFile:write("\n") + metricsFile:write("}\n") + metricsFile:write("\n") + metricsFile:write("registerMetrics(\""..name.."\", "..name.."ImagesTable, "..name.."MetricsTable)\n") + + io.close(metricsFile) + end + + for k, v in pairs(originalLayerVisible) do + local layer = layerLookup[k] + layer.isVisible = v + end + + sprite.properties(pluginKey).previousExportFilename = data.metrics_filename +end diff --git a/export-p8-font-metrics.lua b/export-p8-font-metrics.lua new file mode 100644 index 0000000..b1003ee --- /dev/null +++ b/export-p8-font-metrics.lua @@ -0,0 +1,59 @@ +local pluginKey = "bts/p8-font-metrics" + +local sprite = app.sprite + +local name = app.fs.fileTitle(sprite.filename) + +local previousExportFilename = sprite.properties(pluginKey).previousExportFilename + +if previousExportFilename == nil then + local defaultPath = app.fs.filePath(sprite.filename) + local defaultFilename = name..".lua" + previousExportFilename = app.fs.joinPath(defaultPath, defaultFilename) +end + +local dlg = Dialog() +dlg:file{ id="metrics_filename", label="Metrics output:", filename=previousExportFilename, filetypes=".lua", save=true } +dlg:button{ id="confirm", text="Confirm" } +dlg:button{ id="cancel", text="Cancel" } +dlg:show() + +local data = dlg.data +if data.confirm then + name = app.fs.fileTitle(data.metrics_filename) + + local basePath = app.fs.filePath(data.metrics_filename) + local imagePath = app.fs.joinPath(basePath, name) + + local characterDetails = {} + + for cy=0,15 do + for cx=0,15 do + local x,y=cx*8,cy*8 + local width = 8 + for i, slice in ipairs(sprite.slices) do + if slice.bounds.x == x and slice.bounds.y == y then + width = slice.bounds.width+1 + break + end + end + table.insert(characterDetails, width) + end + end + + if true then + --local metricsFilename = app.fs.joinPath(metricsPath, data.metrics_filename) + local metricsFile = io.open(data.metrics_filename, "w") + + metricsFile:write(name.."Widths = split[[") + for k,v in ipairs(characterDetails) do + metricsFile:write(v .. ",") + end + metricsFile:write("]]\n") + metricsFile:write("\n") + + io.close(metricsFile) + end + + sprite.properties(pluginKey).previousExportFilename = data.metrics_filename +end diff --git a/export-puzzlescript.lua b/export-puzzlescript.lua new file mode 100644 index 0000000..31ef326 --- /dev/null +++ b/export-puzzlescript.lua @@ -0,0 +1,618 @@ +local plugin_key = "bts/puzzlescript" + +local sprite = app.sprite +local pc = app.pixelColor + + + +function hexColor(c) + return string.format("#%02x%02x%02x", c.red, c.green, c.blue) +end + +function findOrInsertIdx(table, value) + for i=1,#table do + if table[i] == value then + return i + end + end + table[#table + 1] = value + return #table +end + +function findOrAdd(table, idx, default) + if table[idx] == nil then + table[idx] = default or {} + end + return table[idx] +end + +function get_layers_flat(output, layer_container) + for _, layer in ipairs(layer_container.layers) do + table.insert(output, layer) + if layer.isGroup then + get_layers_flat(output, layer) + end + end + + return output +end + + + +local PuzzleScriptSection = {} + +function PuzzleScriptSection:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + o.items = {} + return o +end + +function PuzzleScriptSection:add_item(item) + self.items[#self.items + 1] = item +end + + + +local PuzzleScriptRawItem = {} + +function PuzzleScriptRawItem:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + o.lines = {} + return o +end + +function PuzzleScriptRawItem:add_line(line) + self.lines[#self.lines + 1] = line +end + +function PuzzleScriptRawItem:export(script_file) + for _, section_item_line in pairs(self.lines) do + script_file:write(section_item_line) + script_file:write("\n") + end +end + + + +local PuzzleScriptObjectItem = {} + +function PuzzleScriptObjectItem:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + o.colors = {} + o.pixels = {} + return o +end + +function PuzzleScriptObjectItem:set_color(color) + if color.alpha == 255 then + self.colors = { hexColor(color) } + else + self.colors = { "transparent" } + end + self.pixels = nil +end + +function PuzzleScriptObjectItem:set_pixel(x, y, color) + local pixel_idx = y * 5 + x + 1 + local pixel_value = "." + if color.alpha == 255 then + pixel_value = findOrInsertIdx(self.colors, hexColor(color)) - 1 + end + + self.pixels[pixel_idx] = pixel_value +end + +function PuzzleScriptObjectItem:export(script_file) + script_file:write(self.id) + script_file:write("\n") + + script_file:write(table.concat(self.colors, " ")) + script_file:write("\n") + + if self.pixels then + for y = 0, 4 do + for x = 0, 4 do + script_file:write(self.pixels[y * 5 + x + 1]) + end + script_file:write("\n") + end + end +end + + + +local PuzzleScriptLayerItem = {} + +function PuzzleScriptLayerItem:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + o.objects = {} + return o +end + +function PuzzleScriptLayerItem:add_object(object) + self.objects[#self.objects + 1] = object +end + +function PuzzleScriptLayerItem:export(script_file) + local object_ids = { self.id } + for i, obj in ipairs(self.objects) do + object_ids[i] = obj.id + end + script_file:write(table.concat(object_ids, ", ")) + script_file:write("\n") +end + + + +local PuzzleScriptLegendItem = {} + +function PuzzleScriptLegendItem:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self +-- o.objects = {} + return o +end + +function PuzzleScriptLegendItem:obj_match(objects) + if objects == nil and self.objects == nil then return true end + if objects == nil or self.objects == nil then return false end + if #objects ~= #self.objects then return false end + for i = 1, #self.objects do + if objects[i] ~= self.objects[i] then return false end + end + return true +end + +function PuzzleScriptLegendItem:set_objects(objects) + self.objects = {} + for _, obj in pairs(objects) do + table.insert(self.objects, obj) + end +end + +function PuzzleScriptLegendItem:export(script_file) + local object_ids = {} + + if self.objects then + for i, obj in ipairs(self.objects) do + object_ids[i] = obj.id + end + else + object_ids = { "Background" } -- FIXME + end + + script_file:write(self.id) + script_file:write(" = ") + script_file:write(table.concat(object_ids, " and ")) + + script_file:write("\n") +end + + + +local PuzzleScriptLegendMapping = {} + +function PuzzleScriptLegendMapping:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +function PuzzleScriptLegendMapping:add_objects(objects) + self.objects = self.objects or {} + for _, obj in pairs(objects) do + table.insert(self.objects, obj) + end +end + +function PuzzleScriptLegendMapping:export(script_file) + local object_ids = {} + + if self.objects then + for i, obj in ipairs(self.objects) do + object_ids[i] = obj.id + end + end + + -- Failsafe for matching name and single item + if #object_ids == 1 and object_ids[1] == self.id then return end + + script_file:write(self.id) + script_file:write(" = ") + script_file:write(table.concat(object_ids, " or ")) + + script_file:write("\n") +end + + + + +local PuzzleScriptLevelItem = {} + +function PuzzleScriptLevelItem:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + o.tiles = {} + return o +end + +function PuzzleScriptLevelItem:set_tile(tile_x, tile_y, value) + local tile_idx = tile_y * self.width + tile_x + if self.tiles[tile_idx] == nil then + self.tiles[tile_idx] = {} + end + + table.insert(self.tiles[tile_idx], value) +end + +function PuzzleScriptLevelItem:map_to_legend(legend_section, legend_icons) + self.tile_legends = {} + + for tile_y = 0, self.height - 1 do + for tile_x = 0, self.width - 1 do + local tile_idx = tile_y * self.width + tile_x + + local legend_item = nil + for _, test_legend_item in pairs(legend_section.items) do + if test_legend_item:obj_match(self.tiles[tile_idx]) then + legend_item = test_legend_item + break + end + end + + if legend_item == nil then + legend_item = PuzzleScriptLegendItem:new() + assert(#legend_icons > 0) + legend_item.id = legend_icons[1] + table.remove(legend_icons, 1) + legend_item:set_objects(self.tiles[tile_idx]) + legend_section:add_item(legend_item) + end + + self.tile_legends[tile_idx] = legend_item + end + end +end + +function PuzzleScriptLevelItem:export(script_file) + for tile_y = 0, self.height - 1 do + for tile_x = 0, self.width - 1 do + local tile_idx = tile_y * self.width + tile_x + script_file:write(self.tile_legends[tile_idx].id) + end + script_file:write("\n") + end +end + + + +local PuzzleScriptFile = {} + +function PuzzleScriptFile:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + o.sections = {} + return o +end + +function PuzzleScriptFile:get_section(id) + id = string.upper(id) + return findOrAdd(self.sections, id, PuzzleScriptSection:new{}) +end + +function PuzzleScriptFile:read(filename) + local script_file = io.open(filename, "r") + + if script_file then + local current_section = self:get_section("PRELUDE") + local current_item = nil + while true do + local line = script_file:read() + if line == nil then break end + if string.match(line, "^=+$") then + local current_section_name = script_file:read() + current_section = self:get_section(current_section_name) + script_file:read() -- Skip the next line as it's just the closing === + + current_item = nil + elseif string.match(line, "^[ \t\r\n]*$") then + current_item = nil + else + if current_item == nil then + current_item = PuzzleScriptRawItem:new{} + current_section:add_item(current_item) + end + + current_item:add_line(line) + end + end + + io.close(script_file) + end +end + +function PuzzleScriptFile:export(filename) + local script_file = io.open(filename, "w") + + local ordered_section_names = { + "PRELUDE", + "OBJECTS", + "LEGEND", + "SOUNDS", + "COLLISIONLAYERS", + "RULES", + "WINCONDITIONS", + "LEVELS" + } + + for _, section_name in pairs(ordered_section_names) do + local section = self:get_section(section_name) + if section ~= nil then + if section_name ~= "PRELUDE" then + local separator = string.rep("=", string.len(section_name)) + script_file:write(separator) + script_file:write("\n") + script_file:write(section_name) + script_file:write("\n") + script_file:write(separator) + script_file:write("\n\n") + end + + for _, section_item in pairs(section.items) do + section_item:export(script_file) + + if section_name ~= "COLLISIONLAYERS" and section_name ~= "LEGEND" then -- fixme, table?? + script_file:write("\n") + end + end + + if section_name == "COLLISIONLAYERS" or section_name == "LEGEND" then -- fixme, table?? + script_file:write("\n") + end + end + end + + io.close(script_file) +end + + + +function main() + local name = app.fs.fileTitle(sprite.filename) + + local previous_export_filename = sprite.properties(plugin_key).previous_export_filename + local previous_merge_filename = sprite.properties(plugin_key).previous_merge_filename + local previous_export_lofi = sprite.properties(plugin_key).previous_export_lofi + + if previous_export_filename == nil then + local default_path = app.fs.filePath(sprite.filename) + local default_filename = name..".txt" + previous_export_filename = app.fs.joinPath(default_path, default_filename) + end + + local dlg = Dialog() + dlg:file{ id="script_filename", label="Output:", filename=previous_export_filename, filetypes=".txt", save=true } + dlg:file{ id="merge_filename", label="Merge script (optional):", filename=previous_merge_filename, filetypes=".txt" } + dlg:check{ id="lofi", label="Prototype export (layer colours)", selected=previous_export_lofi } + dlg:button{ id="confirm", text="Confirm" } + dlg:button{ id="cancel", text="Cancel" } + dlg:show() + + local data = dlg.data + if data.confirm == false then + return + end + + local layers = get_layers_flat({}, sprite) + + local pz_file = PuzzleScriptFile:new{} + + + local object_section = pz_file:get_section("OBJECTS") + + local layer_objects = {} + + for _, layer in ipairs(layers) do + if layer.isVisible then + if layer.isImage == true and (layer.isTilemap == false or data.lofi) then + local object_item = PuzzleScriptObjectItem:new() + object_item.id = layer.name + object_section:add_item(object_item) + object_item:set_color(layer.color) + elseif layer.isTilemap == true then + local tileset = layer.tileset + + local used_tiles = {} + for _, frame in ipairs(sprite.frames) do + local cel = layer:cel(frame) + if cel ~= nil then + for pixel in cel.image:pixels() do + p = pc.tileI(pixel()) + if p > 0 then + used_tiles[p] = true + end + end + end + end + + local object_names = {} + for object_name in string.gmatch(layer.data, "[^,]+") do + if object_name ~= "" then + object_names[#object_names + 1] = object_name + used_tiles[#object_names] = true -- Always export named tiles + end + end + + for tile_idx, _ in pairs(used_tiles) do + local object_item = PuzzleScriptObjectItem:new() + object_section:add_item(object_item) + + if object_names[tile_idx] then + object_item.id = object_names[tile_idx] + else + object_item.id = layer.name..tile_idx + end + + object_item.layer = layer + + local tile = tileset:tile(tile_idx) + + if tile then + local layer_tiles = findOrAdd(layer_objects, layer) + layer_tiles[tile_idx] = object_item + + local colors = {} + local pixels = {} + for pixel in tile.image:pixels() do + object_item:set_pixel(pixel.x, pixel.y, Color(pixel())) + end + else + assert(false, "Missing tile: " .. layer.name .. " " .. tile_idx) + end + end + end + end + end + + local levels_section = pz_file:get_section("LEVELS") + + local tiles_w = math.floor(sprite.width / 5) + local tiles_h = math.floor(sprite.height / 5) + + for _, frame in ipairs(sprite.frames) do + local level_item = PuzzleScriptLevelItem:new() + levels_section:add_item(level_item) + level_item.width = tiles_w + level_item.height = tiles_h + + for tile_y = 0, tiles_h - 1 do + for tile_x = 0, tiles_w - 1 do + for _, layer in ipairs(layers) do + if layer.isVisible and layer.isTilemap == true then + local cel = layer:cel(frame) + if cel ~= nil then + local image = cel.image + + local offset_x = math.floor(cel.bounds.x / 5) + local offset_y = math.floor(cel.bounds.y / 5) + local cel_width = math.floor(cel.bounds.width / 5) + local cel_height = math.floor(cel.bounds.height / 5) + + local actual_tile_x = tile_x - offset_x + local actual_tile_y = tile_y - offset_y + if actual_tile_x >= 0 and actual_tile_y >= 0 and actual_tile_x < cel_width and actual_tile_y < cel_height then + local pixel_value = image:getPixel(actual_tile_x, actual_tile_y) + local tile_idx = pc.tileI(pixel_value) + if tile_idx > 0 then + assert(layer_objects[layer][tile_idx], "Looking for: " .. layer.name) + level_item:set_tile(tile_x, tile_y, layer_objects[layer][tile_idx]) + end + end + end + end + end + end + end + end + + local legend_section = pz_file:get_section("LEGEND") + + local background_legend = PuzzleScriptLegendItem:new() + background_legend.id = "." + legend_section:add_item(background_legend) + + local legend_icons = {} + for icon in string.gmatch("!\"#$%&'*+,-/\\0123456789:;?@_`abcdefghijklmnopqrstuvwzyz~", ".") do + legend_icons[#legend_icons + 1] = icon + end + + for _, level in pairs(levels_section.items) do + level:map_to_legend(legend_section, legend_icons) + end + + + + + local layers_section = pz_file:get_section("COLLISIONLAYERS") + for _, layer in ipairs(sprite.layers) do + local layer_item = PuzzleScriptLayerItem:new() + layer_item.id = layer.name + layers_section:add_item(layer_item) + end + + + + for layer, objects in pairs(layer_objects) do + local legend_map = PuzzleScriptLegendMapping:new() + legend_map.id = layer.name + legend_map:add_objects(objects) + legend_section:add_item(legend_map) + end + + for _, layer in ipairs(layers) do + if layer.isGroup and #layer.layers > 0 then + local legend_map = PuzzleScriptLegendMapping:new() + legend_map.id = layer.name + for _, sub_layer in ipairs(layer.layers) do + legend_map:add_objects({ {id=sub_layer.name} }) + end + legend_section:add_item(legend_map) + end + end + +-- for _, objects in pairs(layer_objects) do +-- for _, object in pairs(objects) do +-- local layer_item = PuzzleScriptLayerItem:new() +-- layers_section:add_item(layer_item) +-- layer_item.id = object.layer.name +-- end +-- end + + +-- for _, layer in ipairs(layers) do +-- if layer.isVisible and layer.parent == sprite then + + + +--PuzzleScriptLegendMapping:new() + +-- local layer_item = PuzzleScriptLayerItem:new() +-- layers_section:add_item(layer_item) +-- layer_item.id = layer.name +-- end +-- end + + +--legend stuff again after... + + + + -- Merge after making everything else + pz_file:read(data.merge_filename) + + pz_file:export(data.script_filename) + + local export_filename_changed = sprite.properties(plugin_key).previous_export_filename ~= data.script_filename + local merge_filename_changed = sprite.properties(plugin_key).previous_merge_filename ~= data.merge_filename + local lofi_changed = sprite.properties(plugin_key).previous_export_lofi ~= data.lofi + + if export_filename_changed or merge_filename_changed or lofi_changed then + sprite.properties(plugin_key).previous_export_filename = data.script_filename + sprite.properties(plugin_key).previous_merge_filename = data.merge_filename + sprite.properties(plugin_key).previous_export_lofi = data.lofi + end +end + +main()