123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- ----------------------------------------------------------------
- -- UTILITY FUNCTIONS
- ----------------------------------------------------------------
- local _format = string.format
- local _match = string.match
- local _gmatch = string.gmatch
- local _sub = string.sub
- local _find = string.find
- local _insert = table.insert
- local _concat = table.concat
- local _remove = table.remove
- local clamp = function(x, a, b)
- if x < a then return a end
- if x > b then return b end
- return x
- end
- local _tb = function(x) return _floor(clamp(x, 0, 1) * 255 + 0.5) end
- local colorToBytes = love.math.colorToBytes or function(r, g, b, a)
- if type(r) == "table" then
- r, g, b, a = r[1], r[2], r[3], r[4]
- end
- return _tb(r), _tb(g), _tb(b), a and _tb(a)
- end
- local _fb = function(x) return clamp(_floor(x + 0.5) / 255, 0, 1) end
- local colorFromBytes = love.math.colorFromBytes or function(r, g, b, a)
- if type(r) == "table" then
- r, g, b, a = r[1], r[2], r[3], r[4]
- end
- return _fb(r), _fb(g), _fb(b), a and _fb(a)
- end
- --local default_color = function() return {1, 1, 1, 1} end
- local clear_unused = function(cache)
- for k, v in pairs(cache) do
- if not v.used then cache[k] = nil end
- end
- end
- local invalidate = function(cache)
- for k, v in pairs(cache) do v.used = false end
- end
- local split = function(s, sep)
- local t = {}; local init = 1; local m, n
- sep = sep or '\n'
- while true do
- m, n = _find(s, sep, init, true)
- if m == nil then
- _insert(t, _sub(s, init))
- break
- end
- _insert(t, _sub(s, init, m - 1))
- init = n + 1
- end
- return t
- end
- -- returns a table repr. of normalized path: an npath
- -- nparent is the npath containing path
- -- nparent is either nil or already normalized
- local normalize_path = function(path, nparent)
- path = split(path, "/")
- local npath = {}
- local skip = 0
- for i = #path, 1, -1 do
- local v = path[i]
- if v == ".." then skip = skip + 1
- elseif v ~= "." and v ~= "" then
- if skip > 0 then skip = skip - 1
- else _insert(npath, 1, v) end
- end
- end
-
- if nparent then
- for i = #nparent - skip, 1, -1 do
- _insert(npath, 1, nparent[i])
- end
- else
- path.skip = skip
- nparent = path
- end
- if nparent[1] == "" then _insert(npath, 1, "") end
- return npath
- end
- local _sx = function(s, i, j)
- return tonumber(_sub(s, i, j), 16)
- end
- local parse_colorbytes = function(s, require_crunch)
- if not s then return end
- local crunch, s = _match(s, '^(#?)(%x+)')
- if not s or (require_crunch and crunch ~= "#") then
- print(_format("not a color string:%s", s))
- return
- end
-
- local R, G, B, A
- if #s == 6 then -- #RRGGBB
- R, G, B = _sx(s, 1, 2), _sx(s, 3, 4), _sx(s, 5, 6)
- elseif #s == 8 then -- #AARRGGBB
- A, R, G, B = _sx(s, 1, 2), _sx(s, 3, 4), _sx(s, 5, 6), _sx(s, 7, 8)
- else
- print(_format("unsupported color string: %s%s", crunch, s))
- return
- end
- return R, G, B, A
- end
- local parse_color = function(s, crunch)
- local r,g,b,a = parse_colorbytes(s, crunch)
- if r then return {colorFromBytes(r, g, b, a)} end
- end
- ----------------------------------------------------------------
- -- LOADER INIT
- ----------------------------------------------------------------
- local Loader = {
- filter = {
- min = "nearest", -- min filter mode
- mag = "nearest", -- mag filter mode
- anisotropy = 1, -- max anisotropy
- },
- useSpriteBatch = true,
- drawObjects = true,
- cache = {},
- }
- local cache = Loader.cache
- local PATH = (...):gsub("[\\/]", "."):match(".+%.") or ''
- local xml = require(PATH .. "xml")
- ----------------------------------------------------------------
- -- SECTION PARSERS
- ----------------------------------------------------------------
- local parse_properties = function(t)
- local props = {}
- for _,v in ipairs(t) do
- if v._name == "property" then
- local attr = v._attr
- local ptype, pvalue = attr.type, attr.value
- if ptype == "bool" then
- if pvalue == "true" then pvalue = true
- elseif pvalue == "false" then pvalue = false
- else pvalue = nil end
- elseif ptype == "int" or ptype == "float" then
- pvalue = tonumber(pvalue)
- elseif ptype == "color" then
- if pvalue == "" then pvalue = {1, 1, 1, 1}
- else pvalue = parse_color(pvalue, true) end
- elseif ptype == "file" then
- pvalue = {
- path = pvalue,
- filepath = _concat(normalize_path(pvalue, Loader.npath), "/"),
- }
- end
-
- props[attr.name] = pvalue
- end
- end
- return props
- end
- local parse_tileset = function(t)
- local attr = t._attr
- local firstgid = tonumber(attr.firstgid)
- local tsx = attr.source
- if tsx then -- external tileset
- local path = _concat(normalize_path(tsx, Loader.npath), "/")
- t = xml.parse(love.filesystem.read(path))
- for _,v in ipairs(t) do
- if v._name == "tileset" then t = v break end
- end
- attr = t._attr
- attr.firstgid = firstgid
- --attr.source = tsx
- end
- local tileset = {
- firstgid = firstgid,
- --source = tsx,
- name = attr.name,
- tileWidth = tonumber(attr.tilewidth),
- tileHeight = tonumber(attr.tileheight),
- spacing = tonumber(attr.spacing) or 0,
- margin = tonumber(attr.margin) or 0,
- tileCount = tonumber(attr.tilecount),
- columns = tonumber(attr.columns),
- }
-
- local props
- local tileProperties = {}
- local tileTypes = {}
- local offsetX, offsetY = 0, 0
- local cached
-
- for _, v in ipairs(t) do
- local vname, vattr = v._name, v._attr
- if vname == "image" then
- assert(cached == nil, "multiple image sections not supported")
- local source = vattr.source
- local path = _concat(normalize_path(source, Loader.npath), "/")
- cached = cache[path]
- if not cached then
- local image = love.image.newImageData(path) -- exists?
- local trans = vattr.trans
- if trans then
- local R, G, B = parse_colorbytes(trans)
- local rb, gb, bb
- image:mapPixel( function(x, y, r, g, b, a)
- rb, gb, bb = colorToBytes(r, g, b)
- if R == rb and G == gb and B == bb then return r, g, b, 0 end
- return r,g,b,a
- end)
- trans = {colorFromBytes(R, G, B)}
- end
- local filter = Loader.filter
- image = love.graphics.newImage(image)
- image:setFilter(filter.min, filter.mag, filter.anisotropy)
- cached = {image = image, trans = trans, source = source}
- cached.width, cached.height = image:getDimensions()
- end
- cached.used = true
- cache[path] = cached
- elseif vname == "tile" then
- local tilegid = firstgid + vattr.id
- if vattr.type then tileTypes[tilegid] = vattr.type end
- for _, v2 in ipairs(v) do
- if v2._name == "properties" then
- tileProperties[tilegid] = parse_properties(v2)
- break
- end
- end
- elseif vname == "properties" then
- assert(cached == nil, "multiple properties sections not supported")
- props = parse_properties(v)
- elseif vname == "tileoffset" then
- assert(offsetX == nil, "multiple tileoffset sections not supported")
- offsetX, offsetY = vattr.x, vattr.y
- end
- end
- assert(cached, "parse_tileset - tileset does not contain an image")
-
- tileset.image = cached.image
- tileset.imageTrans = cached.trans
- tileset.imagePath = cached.source
- tileset.width = cached.width
- tileset.height = cached.height
-
- tileset.offsetX = tonumber(offsetX) or 0
- tileset.offsetY = tonumber(offsetY) or 0
-
- tileset.properties = props or {}
- tileset.tileProperties = tileProperties
- tileset.tileTypes = tileTypes
- return tileset
- end
- local parse_tilelayer_data = function(t)
- local data = {}
- local encoding = t._attr.encoding
-
- if encoding == nil then -- xml
- for k, v in ipairs(t) do
- if v._name == "tile" then
- _insert(data, tonumber(v._attr.gid) or 0)
- end
- end
- elseif encoding == "csv" then
- for s in _gmatch(t[1], "%d+") do
- _insert(data, tonumber(s))
- end
- elseif encoding == "base64" then
- local compfmt = t._attr.compression
- local decoded = love.data.decode("string", "base64", t[1])
-
- if compfmt == "gzip" or compfmt == "zlib" then
- decoded = love.data.decompress("string", compfmt, decoded)
- end
-
- local pos = 1
- for i = 1, math.floor(#decoded / 4) do
- data[i], pos = love.data.unpack("<I4", decoded, pos)
- end
- else
- --data = nil
- print("parse_tilelayer_data - unsupported encoding")
- end
-
- return data
- end
- local parse_tilelayer = function(t)
- local attr = t._attr
- local layer = {
- id = tonumber(attr.id),
- name = attr.name or ("Tile Layer " .. attr.id),
- width = tonumber(attr.width),
- height = tonumber(attr.height),
- opacity = tonumber(attr.opacity) or 1,
- visible = tonumber(attr.visible) ~= 0,
- offsetX = tonumber(attr.offsetx) or 0,
- offsetY = tonumber(attr.offsety) or 0,
- }
-
- local data, props
- for _, v in ipairs(t) do
- local vname = v._name
- if vname == "data" then
- assert(data == nil, "multiple data sections not supported")
- data = parse_tilelayer_data(v)
- elseif vname == "properties" then
- assert(props == nil, "multiple properties sections not supported")
- props = parse_properties(v)
- end
- end
-
- layer.properties = props or {}
- layer.data = data
- layer.type = "tilelayer"
- return layer
- end
- local parse_objectlayer = function(t)
- local attr = t._attr
- local layer = {
- id = tonumber(attr.id),
- name = attr.name or ("Object Layer " .. attr.id),
- color = parse_color(attr.color, true) or {0.5, 0.5, 0.5, 1},
- opacity = tonumber(attr.opacity) or 1,
- visible = tonumber(attr.visible) ~= 0,
- offsetX = tonumber(attr.offsetx) or 0,
- offsetY = tonumber(attr.offsety) or 0,
- drawOrder = attr.draworder or "topdown"
- }
-
- local objects = {}
- local props
- for _, v in ipairs(t) do
- if v._name == "object" then
- local vattr = v._attr
- local obj = {
- id = tonumber(vattr.id),
- name = vattr.name or "",
- type = vattr.type or "",
- x = tonumber(vattr.x),
- y = tonumber(vattr.y),
- width = tonumber(vattr.width) or 0,
- height = tonumber(vattr.height) or 0,
- rotation = tonumber(vattr.rotation) or 0,
- gid = tonumber(vattr.gid),
- visible = tonumber(vattr.visible) ~= 0,
- }
- _insert(objects, obj)
- for _, v2 in ipairs(v) do
- local v2name = v2._name
- if v2name == "properties" then
- obj.properties = parse_properties(v2)
- elseif v2name == "ellipse" then obj.ellipse = true
- elseif v2name == "point" then obj.point = true
- elseif v2name == "text" then obj.text = v2[1] or ""
- elseif v2name == "polyline" then
- obj.polyline = {}
- for num in _gmatch(v2._attr.points, "-?%d+") do
- _insert(obj.polyline, tonumber(num))
- end
- elseif v2name == "polygon" then
- obj.polygon = {}
- for num in _gmatch(v2._attr.points, "-?%d+") do
- _insert(obj.polygon, tonumber(num))
- end
- end
- end
- elseif v._name == "properties" then
- props = parse_properties(v)
- end
- end
-
- layer.properties = props or {}
- layer.objects = objects
- layer.type = "objectgroup"
-
- return layer
- end
- -- parse map
- local parse_map = function(t)
- local attr = t._attr
- local map = {
- version = attr.version,
- tiledVersion = attr.tiledversion,
- orientation = attr.orientation,
- renderOrder = attr.renderorder,
- width = tonumber(attr.width),
- height = tonumber(attr.height),
- tileWidth = tonumber(attr.tilewidth),
- tileHeight = tonumber(attr.tileheight),
- hexsidelength = tonumber(attr.hexsidelength),
- staggerAxis = attr.staggeraxis,
- staggerIndex = attr.staggerindex,
- backgroundColor = parse_color(attr.backgroundcolor, true),-- or {0, 0, 0, 1},
- infinite = tonumber(attr.infinite) == 1,
- nextLayerID = tonumber(attr.nextlayerid),
- nextObjectID = tonumber(attr.nextobjectid),
- }
-
- local props
- local tilesets, layers = {}, {}
- for _, v in ipairs(t) do
- local vname = v._name
- if vname == "properties" then
- assert(props == nil, "multiple properties sections not supported")
- props = parse_properties(v)
- elseif vname == "tileset" then
- local tileset = parse_tileset(v, map)
- tilesets[tileset.name] = tileset
- elseif vname == "layer" then
- _insert(layers, parse_tilelayer(v))
- elseif vname == "objectgroup" then
- _insert(layers, parse_objectlayer(v))
- end
- end
-
- map.properties = props or {}
- map.tilesets = tilesets
- map.layerOrder = layers
-
- return map
- end
- ----------------------------------------------------------------
- -- LOADS A TMX FILE
- ----------------------------------------------------------------
- local Map = require(PATH .. "Map")
- local TileSet = require(PATH .. "TileSet")
- local TileLayer = require(PATH .. "TileLayer")
- local Object = require(PATH .. "Object")
- local ObjectLayer = require(PATH .. "ObjectLayer")
- Loader.load = function(filepath)
- local npath = normalize_path(filepath)
- Loader.filename = _remove(npath)
- Loader.filedir = _concat(npath, "/")
- Loader.filepath = Loader.filedir .. "/" .. Loader.filename
- Loader.npath = npath
-
- filepath = Loader.filepath
-
- if not love.filesystem.getInfo(filepath, "file") then
- print("Loader.load - could not find the file: " .. filepath)
- return
- end
- local parsed = xml.parse(love.filesystem.read(filepath))
-
- local map
- for i, v in ipairs(parsed) do
- if v._name == "map" then
- invalidate(cache)
- map = parse_map(v)
- clear_unused(cache)
- break
- end
- end
- if not map then
- print("Loader.load - missing map section in file: " .. filepath)
- return
- end
-
- map.directory = Loader.filedir
- map.name = Loader.filename
- map.useSpriteBatch = Loader.useSpriteBatch
- map.drawObjects = Loader.drawObjects
- map.visible = true
-
- Map.init(map)
-
- for k, v in pairs(map.tilesets) do
- v.map = map
- TileSet.init(v)
- end
- map:updateTiles()
-
- map.layers = {}
- for i, v in ipairs(map.layerOrder) do
- v.map = map
- if v.type == "tilelayer" then
- v.useSpriteBatch = map.useSpriteBatch
- TileLayer.init(v)
- v:_populate(v.data)
- v.data = nil
- elseif v.type == "objectgroup" then
- ObjectLayer.init(v)
- for _, obj in ipairs(v.objects) do
- obj.layer = v
- Object.init(obj)
- end
- end
- map.layers[v.name] = v
- --map.layerOrder[#map.layerOrder + 1] = v
- end
-
- return map
- end
- ----------------------------------------------------------------
- -- SAVES A MAP AS TMX (REMOVED)
- ----------------------------------------------------------------
- local xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n'
- local defaultSavePath = "Saved Maps"
- -- save a tmx file
- function Loader.save(map, filename, path)
- path = path or defaultSavePath
- if not love.filesystem.getInfo(path, "directory") then
- love.filesystem.createDirectory(path)
- end
- --love.filesystem.write(path .. "/" .. filename,
- -- xmlHeader .. xml.toxml(write_map(map)) )
- end
- return Loader
|