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 = {} 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") if #self.colors > 0 then 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 else script_file:write("transparent\n") 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_selected_only = sprite.properties(plugin_key).previous_export_selected_only 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="selected_only", label="Export selected level(s) only", selected=previous_export_selected_only } 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) local exported_frames = sprite.frames if data.selected_only then if app.range.isEmpty == false then exported_frames = app.range.frames else exported_frames = { app.frame } end end for _, frame in ipairs(exported_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 -- 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 selected_only_changed = sprite.properties(plugin_key).previous_export_selected_only ~= data.selected_only local lofi_changed = sprite.properties(plugin_key).previous_export_lofi ~= data.lofi if export_filename_changed or merge_filename_changed or selected_only_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_selected_only = data.selected_only sprite.properties(plugin_key).previous_export_lofi = data.lofi end end main()