added utilities for tilesets and maps

This commit is contained in:
Andre Leiradella 2015-05-03 18:19:44 -03:00
parent bc0bd670a2
commit 8dae2efd24
2 changed files with 661 additions and 0 deletions

524
etc/rlmap.lua Normal file
View File

@ -0,0 +1,524 @@
local image = require 'image'
local path = require 'path'
local xml = [===[
local function prettyPrint( node, file, ident )
file = file or io.stdout
ident = ident or 0
if type( node ) == 'table' then
file:write( ( ' ' ):rep( ident ), '<', node.label )
for attr, value in pairs( node.xarg ) do
file:write( ' ', attr, '="', value, '"' )
end
if node.empty then
file:write( '/>\n' )
else
file:write( '>\n' )
for _, child in ipairs( node ) do
prettyPrint( child, file, ident + 2 )
end
file:write( ( ' ' ):rep( ident ), '</', node.label, '>\n' )
end
else
file:write( ( ' ' ):rep( ident ), node, '\n' )
end
end
local function findNode( node, label )
if type( node ) == 'table' then
if node.label == label then
return node
end
for i = 1, #node do
local res = findNode( node[ i ], label )
if res then
return res
end
end
end
end
local function parseargs(s)
local arg = {}
string.gsub(s, "([%w:]+)=([\"'])(.-)%2", function (w, _, a)
arg[w] = a
end)
return arg
end
return
{
parse = function(s)
local stack = {}
local top = {}
table.insert(stack, top)
local ni,c,label,xarg, empty
local i, j = 1, 1
while true do
ni,j,c,label,xarg, empty = string.find(s, "<(%/?)([%w:]+)(.-)(%/?)>", i)
if not ni then break end
local text = string.sub(s, i, ni-1)
if not string.find(text, "^%s*$") then
table.insert(top, text)
end
if empty == "/" then -- empty element tag
table.insert(top, {label=label, xarg=parseargs(xarg), empty=1})
elseif c == "" then -- start tag
top = {label=label, xarg=parseargs(xarg)}
table.insert(stack, top) -- new level
else -- end tag
local toclose = table.remove(stack) -- remove top
top = stack[#stack]
if #stack < 1 then
error("nothing to close with "..label)
end
if toclose.label ~= label then
error("trying to close "..toclose.label.." with "..label)
end
table.insert(top, toclose)
end
i = j+1
end
local text = string.sub(s, i)
if not string.find(text, "^%s*$") then
table.insert(stack[#stack], text)
end
if #stack > 1 then
error("unclosed "..stack[#stack].label)
end
return stack[1][1]
end,
findNode = findNode,
findAttr = function( node, name )
for key, value in pairs( node.xarg ) do
if key == name then
return value
end
end
end,
prettyPrint = prettyPrint
}
]===]
xml = load( xml, 'xml.lua' )()
local function dump( t, i )
i = i or 0
local s = string.rep( ' ', i * 2 )
if type( t ) == 'table' then
io.write( s, '{\n' )
for k, v in pairs( t ) do
io.write( s, ' ', tostring( k ), ' = ' )
dump( v, i + 1 )
end
io.write( s, '}\n' )
elseif type( t ) == 'string' then
io.write( s, string.format( '%q', t ), '\n' )
else
io.write( s, tostring( t ), '\n' )
end
end
local function split( str, sep )
sep = sep or ' '
local res = {}
local i = 1
while #str ~= 0 do
local j = str:find( sep, i, true )
if not j then
j = #str + 1
end
res[ #res + 1 ] = str:sub( i, j - 1 )
str = str:sub( j + 1 )
end
return res
end
local function loadtmx( filename )
local dir = path.split( filename ) .. path.separator
local file, err = io.open( filename )
if not file then
error( err )
end
file:read( '*l' ) -- skip <?xml ... ?>
local contents = file:read( '*a' )
file:close()
local tmx = xml.parse( contents )
local map = {}
map.version = xml.findAttr( tmx, 'version' )
map.orientation = xml.findAttr( tmx, 'orientation' )
map.width = tonumber( xml.findAttr( tmx, 'width' ) )
map.height = tonumber( xml.findAttr( tmx, 'height' ) )
map.tilewidth = tonumber( xml.findAttr( tmx, 'tilewidth' ) )
map.tileheight = tonumber( xml.findAttr( tmx, 'tileheight' ) )
map.widthpixels = map.width * map.tilewidth
map.heightpixels = map.height * map.tileheight
map.backgroundcolor = image.color( 0, 0, 0 )
local backgroundcolor = xml.findAttr( tmx, 'backgroundcolor' )
if backgroundcolor then
backgroundcolor = tonumber( backgroundcolor, 16 )
local r = backgroundcolor >> 16
local g = backgroundcolor >> 8 & 255
local b = backgroundcolor & 255
map.backgroundcolor = image.color( r, g, b )
end
local tilesets = {}
map.tilesets = tilesets
local gids = {}
map.gids = gids
for _, child in ipairs( tmx ) do
if child.label == 'tileset' then
local tileset =
{
firstgid = tonumber( xml.findAttr( child, 'firstgid' ) ),
name = xml.findAttr( child, 'name' ),
tilewidth = tonumber( xml.findAttr( child, 'tilewidth' ) ),
tileheight = tonumber( xml.findAttr( child, 'tileheight' ) )
}
if map.tilewidth ~= tileset.tilewidth or map.tileheight ~= tileset.tileheight then
error( string.format( 'tile dimensions in %s are different from tile dimensions in map', tileset.name ) )
end
for _, child2 in ipairs( child ) do
if child2.label == 'image' then
local filename = path.realpath( dir .. xml.findAttr( child2, 'source' ) )
tileset.image = image.load( filename )
local trans = xml.findAttr( child2, 'trans' )
if trans then
trans = tonumber( trans, 16 )
local r = trans >> 16
local g = trans >> 8 & 255
local b = trans & 255
trans = image.color( r, g, b )
tileset.image:colorToAlpha( trans )
end
end
end
tileset.lastgid = tileset.firstgid + ( tileset.image:getWidth() // tileset.tilewidth ) * ( tileset.image:getHeight() // tileset.tileheight ) - 1
local imagewidth = tileset.image:getWidth()
local tilewidth = tileset.tilewidth
local tileheight = tileset.tileheight
for i = tileset.firstgid, tileset.lastgid do
local id = i - tileset.firstgid
local j = id * tileset.tilewidth
local x = j % imagewidth
local y = math.floor( j / imagewidth ) * tileset.tileheight
gids[ i ] =
{
tileset = tileset,
id = id,
x = x,
y = y,
width = tilewidth,
height = tileheight,
image = tileset.image:sub( x, y, x + tilewidth - 1, y + tileheight - 1 )
}
end
tilesets[ #tilesets + 1 ] = tileset
end
end
local layers = {}
map.layers = layers
for _, child in ipairs( tmx ) do
if child.label == 'layer' then
local layer =
{
name = xml.findAttr( child, 'name' ),
width = tonumber( xml.findAttr( child, 'width' ) ),
height = tonumber( xml.findAttr( child, 'height' ) ),
tiles = {}
}
for _, child2 in ipairs( child ) do
if child2.label == 'data' then
local index = 1
for y = 1, layer.height do
local row = {}
layer.tiles[ y ] = row
for x = 1, layer.width do
local tile = child2[ index ]
index = index + 1
row[ x ] = tonumber( xml.findAttr( tile, 'gid' ) )
end
end
end
end
layers[ #layers + 1 ] = layer
end
end
return map
end
local function render( map, layers )
local png = image.create( map.widthpixels, map.heightpixels, image.color( 0, 0, 0, 0 ) )
for _, layer in ipairs( map.layers ) do
if layers[ layer.name ] then
local yy = 0
for y = 1, layer.height do
local row = layer.tiles[ y ]
local xx = 0
for x = 1, layer.width do
if row[ x ] ~= 0 then
local tile = map.gids[ row[ x ] ]
if not tile then
error( 'Unknown gid ' .. row[ x ] .. ' in layer ' .. layer.name )
end
tile.image:blit( png, xx, yy )
end
xx = xx + map.tilewidth
end
yy = yy + map.tileheight
end
end
end
return png
end
local function newwriter()
return {
content = {},
writeu8 = function( self, x )
self.content[ #self.content + 1 ] = string.char( x & 255 )
end,
writeu16 = function( self, x )
self.content[ #self.content + 1 ] = string.char( ( x >> 8 ) & 255, x & 255 )
end,
writeu32 = function( self, x )
self.content[ #self.content + 1 ] = string.char( ( x >> 24 ) & 255, ( x >> 16 ) & 255, ( x >> 8 ) & 255, x & 255 )
end,
prependu32 = function( self, x )
table.insert( self.content, 1, string.char( ( x >> 24 ) & 255, ( x >> 16 ) & 255, ( x >> 8 ) & 255, x & 255 ) )
end,
save = function( self, filename )
local file, err = io.open( filename, 'wb' )
if not file then error( err ) end
file:write( table.concat( self.content ) )
file:close()
end,
size = function( self, filename )
self.content = { table.concat( self.content ) }
return #self.content[ 1 ]
end
}
end
local function compile( map, layers, coll )
local built = { tiles = {}, images = {}, layer0 = {}, layers = {} }
local imgset = {}
local out = newwriter()
-- build layer 0 and the tileset
do
local names = {}
for i = 1, #layers[ 1 ] do
for _, layer in ipairs( map.layers ) do
if layer.name == layers[ 1 ][ i ] then
names[ layers[ 1 ][ i ] ] = layer
end
end
end
local png = render( map, names )
for y = 0, png:getHeight() - 1, map.tileheight do
built.layer0[ y // map.tileheight + 1 ] = {}
for x = 0, png:getWidth() - 1, map.tilewidth do
local sub = png:sub( x, y, x + 31, y + 31 )
if not imgset[ sub:getHash() ] then
built.tiles[ #built.tiles + 1 ] = sub
imgset[ sub:getHash() ] = #built.tiles - 1
end
built.layer0[ y // map.tileheight + 1 ][ x // map.tilewidth + 1 ] = imgset[ sub:getHash() ]
end
end
-- rl_map_t
out:writeu16( map.width )
out:writeu16( map.height )
out:writeu16( 1 ) -- layer count
out:writeu16( 0 ) -- pad
-- rl_tileset_t
out:writeu32( map.tilewidth * map.tileheight * 2 * #built.tiles + 6 ) -- total tileset size
out:writeu16( map.tilewidth )
out:writeu16( map.tileheight )
out:writeu16( #built.tiles )
for _, tile in ipairs( built.tiles ) do
for y = 0, map.tileheight - 1 do
for x = 0, map.tilewidth - 1 do
local r, g, b = image.split( tile:getPixel( x, y ) )
r, g, b = r * 31 // 255, g * 63 // 255, b * 31 // 255
out:writeu16( ( r << 11 ) | ( g << 5 ) | b )
end
end
end
-- rl_layer0
for y = 1, map.height do
for x = 1, map.width do
out:writeu16( built.layer0[ y ][ x ] )
end
end
end
return out
end
return function( args )
if #args < 2 then
io.write[[
An utility to work with Tiled maps and convert them to retroluxury.
rlmap understands the following commands:
* list: Lists the layers and/or tilesets in a map.
* render: Renders the map as a PNG image. The command accepts a list of layer
names and will render only them if the list is given.
* compile: Converts the map into a format ready to be used with rl_map_create.
If a collision layer is given with --coll, all non-zero tiles
represent unpassable tiles. The map resulting layers are a
combination of one or more map layers, they can be compined with
the + operator. Ex.: floor+flobjs fences+walls will create a map
with two layers, the first with the combined floor and flobjs
layers and the other with the fences and walls layers combined.
Usage: rlmap <mapname.tmx> command args...
Commands:
list [--layers] [--tilesets] lists the map\'s layers and/or tilesets
render [layername...] renders the map as a PNG image
compile --coll layermame compiles the map as a .map file
layername[+layername]...
]]
return 0
end
local filename = path.realpath( args[ 1 ] )
local map = loadtmx( filename )
if args[ 2 ] == 'list' then
local layers = false
local tilesets = false
for i = 3, #args do
if args[ i ] == '--layers' then
layers = true
elseif args[ i ] == '--tilesets' then
tilesets = true
else
error( 'unknown argument to list: ' .. args[ i ] )
end
end
if not layers and not tilesets then
layers, tilesets = true, true
end
if layers then
for i, layer in ipairs( map.layers ) do
io.write( string.format( 'Layer %d: %s\n', i, layer.name ) )
end
end
if tilesets then
for i, tileset in ipairs( map.tilesets ) do
io.write( string.format( 'Tileset %d: %s\n', i, tileset.name ) )
end
end
elseif args[ 2 ] == 'render' then
local layers = {}
for i = 3, #args do
layers[ args[ i ] ] = true
end
if not next( layers ) then
for i, layer in ipairs( map.layers ) do
layers[ layer.name ] = true
end
end
local dir, name, ext = path.split( filename )
render( map, layers ):save( dir .. path.separator .. name .. '.png' )
elseif args[ 2 ] == 'compile' then
local layers = {}
local coll = nil
local ii = 3
if args[ 3 ] == '--coll' then
coll = split( args[ 4 ], '+' )
ii = 5
end
for i = ii, #args do
layers[ i - ii + 1 ] = split( args[ i ], '+' )
end
if #layers == 0 then
error( 'the built map must have at least one layer' )
end
local out = compile( map, layers, coll )
local dir, name, ext = path.split( filename )
out:save( dir .. path.separator .. name .. '.map' )
else
error( 'unknown command: ' .. args[ 2 ] )
end
end

137
etc/rltileset.lua Normal file
View File

@ -0,0 +1,137 @@
local image = require 'image'
local path = require 'path'
local function newwriter()
return {
bytes = {},
write8 = function( self, x )
self.bytes[ #self.bytes + 1 ] = string.char( x & 255 )
end,
write16 = function( self, x )
self.bytes[ #self.bytes + 1 ] = string.char( ( x >> 8 ) & 255, x & 255 )
end,
write32 = function( self, x )
self.bytes[ #self.bytes + 1 ] = string.char( ( x >> 24 ) & 255, ( x >> 16 ) & 255, ( x >> 8 ) & 255, x & 255 )
end,
append = function( self, rle )
self.bytes[ #self.bytes + 1 ] = table.concat( rle.bytes )
end,
size = function( self )
self.bytes = { table.concat( self.bytes ) }
return #self.bytes[ 1 ]
end,
save = function( self, name )
local file, err = io.open( name, 'wb' )
if not file then error( err ) end
self.bytes = { table.concat( self.bytes ) }
file:write( self.bytes[ 1 ] )
file:close()
end
}
end
local function rgbto16( r, g, b )
r = r * 31 // 255
g = g * 63 // 255
b = b * 31 // 255
return ( r << 11 ) | ( g << 5 ) | b
end
local function getpixel( png, x, y )
local r, g, b = image.split( png:getPixel( x, y ) )
return rgbto16( r, g, b )
end
local function mktileset( images )
local writer = newwriter()
writer:write16( images[ 1 ]:getWidth() )
writer:write16( images[ 1 ]:getHeight() )
writer:write16( #images )
for _, img in pairs( images ) do
for y = 0, img:getHeight() - 1 do
for x = 0, img:getWidth() - 1 do
writer:write16( getpixel( img, x, y ) )
end
end
end
return writer
end
local function main( args )
if #args == 0 then
io.write[[
rltileset.lua reads all images in the given directory and writes tileset data
that can be directly fed to rl_tileset_create. The first image it finds in the
given directory will dictate the width and height of all tiles in the tileset.
Subsequent images with different dimensions won't be written to the tileset.
Usage: luai rltileset.lua [ options ] <directory>
--output <file> writes the tileset to the given file
(the default is <directory>.tls)
]]
return 0
end
local dir, output
for i = 1, #args do
if args[ i ] == '--output' then
output = args[ i + 1 ]
i = i + 1
else
dir = args[ i ]
end
end
dir = path.realpath( dir )
local files = path.scandir( dir )
local width, height
local images = {}
for _, file in ipairs( files ) do
local stat = path.stat( file )
if stat.file then
local ok, png = pcall( image.load, file )
if ok then
if not width then
width, height = png:getSize()
images[ #images + 1 ] = png
io.write( string.format( '%s set the tileset dimensions to %dx%d\n', file, width, height ) )
elseif width == png:getWidth() and height == png:getHeight() then
images[ #images + 1 ] = png
else
io.write( string.format( '%s doesn\'t have the required dimensions\n', file ) )
end
else
io.write( string.format( '%s could not be read as an image\n', file ) )
end
end
end
if #images ~= 0 then
local writer = mktileset( images )
if not output then
local dir, name, ext = path.split( dir )
output = dir .. path.separator .. name .. '.tls'
end
writer:save( output )
else
io.write( 'no images were found\n' )
end
return 0
end
return main, mktileset