Loader.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. ----------------------------------------------------------------
  2. -- UTILITY FUNCTIONS
  3. ----------------------------------------------------------------
  4. local _format = string.format
  5. local _match = string.match
  6. local _gmatch = string.gmatch
  7. local _sub = string.sub
  8. local _find = string.find
  9. local _insert = table.insert
  10. local _concat = table.concat
  11. local _remove = table.remove
  12. local clamp = function(x, a, b)
  13. if x < a then return a end
  14. if x > b then return b end
  15. return x
  16. end
  17. local _tb = function(x) return _floor(clamp(x, 0, 1) * 255 + 0.5) end
  18. local colorToBytes = love.math.colorToBytes or function(r, g, b, a)
  19. if type(r) == "table" then
  20. r, g, b, a = r[1], r[2], r[3], r[4]
  21. end
  22. return _tb(r), _tb(g), _tb(b), a and _tb(a)
  23. end
  24. local _fb = function(x) return clamp(_floor(x + 0.5) / 255, 0, 1) end
  25. local colorFromBytes = love.math.colorFromBytes or function(r, g, b, a)
  26. if type(r) == "table" then
  27. r, g, b, a = r[1], r[2], r[3], r[4]
  28. end
  29. return _fb(r), _fb(g), _fb(b), a and _fb(a)
  30. end
  31. --local default_color = function() return {1, 1, 1, 1} end
  32. local clear_unused = function(cache)
  33. for k, v in pairs(cache) do
  34. if not v.used then cache[k] = nil end
  35. end
  36. end
  37. local invalidate = function(cache)
  38. for k, v in pairs(cache) do v.used = false end
  39. end
  40. local split = function(s, sep)
  41. local t = {}; local init = 1; local m, n
  42. sep = sep or '\n'
  43. while true do
  44. m, n = _find(s, sep, init, true)
  45. if m == nil then
  46. _insert(t, _sub(s, init))
  47. break
  48. end
  49. _insert(t, _sub(s, init, m - 1))
  50. init = n + 1
  51. end
  52. return t
  53. end
  54. -- returns a table repr. of normalized path: an npath
  55. -- nparent is the npath containing path
  56. -- nparent is either nil or already normalized
  57. local normalize_path = function(path, nparent)
  58. path = split(path, "/")
  59. local npath = {}
  60. local skip = 0
  61. for i = #path, 1, -1 do
  62. local v = path[i]
  63. if v == ".." then skip = skip + 1
  64. elseif v ~= "." and v ~= "" then
  65. if skip > 0 then skip = skip - 1
  66. else _insert(npath, 1, v) end
  67. end
  68. end
  69. if nparent then
  70. for i = #nparent - skip, 1, -1 do
  71. _insert(npath, 1, nparent[i])
  72. end
  73. else
  74. path.skip = skip
  75. nparent = path
  76. end
  77. if nparent[1] == "" then _insert(npath, 1, "") end
  78. return npath
  79. end
  80. local _sx = function(s, i, j)
  81. return tonumber(_sub(s, i, j), 16)
  82. end
  83. local parse_colorbytes = function(s, require_crunch)
  84. if not s then return end
  85. local crunch, s = _match(s, '^(#?)(%x+)')
  86. if not s or (require_crunch and crunch ~= "#") then
  87. print(_format("not a color string:%s", s))
  88. return
  89. end
  90. local R, G, B, A
  91. if #s == 6 then -- #RRGGBB
  92. R, G, B = _sx(s, 1, 2), _sx(s, 3, 4), _sx(s, 5, 6)
  93. elseif #s == 8 then -- #AARRGGBB
  94. A, R, G, B = _sx(s, 1, 2), _sx(s, 3, 4), _sx(s, 5, 6), _sx(s, 7, 8)
  95. else
  96. print(_format("unsupported color string: %s%s", crunch, s))
  97. return
  98. end
  99. return R, G, B, A
  100. end
  101. local parse_color = function(s, crunch)
  102. local r,g,b,a = parse_colorbytes(s, crunch)
  103. if r then return {colorFromBytes(r, g, b, a)} end
  104. end
  105. ----------------------------------------------------------------
  106. -- LOADER INIT
  107. ----------------------------------------------------------------
  108. local Loader = {
  109. filter = {
  110. min = "nearest", -- min filter mode
  111. mag = "nearest", -- mag filter mode
  112. anisotropy = 1, -- max anisotropy
  113. },
  114. useSpriteBatch = true,
  115. drawObjects = true,
  116. cache = {},
  117. }
  118. local cache = Loader.cache
  119. local PATH = (...):gsub("[\\/]", "."):match(".+%.") or ''
  120. local xml = require(PATH .. "xml")
  121. ----------------------------------------------------------------
  122. -- SECTION PARSERS
  123. ----------------------------------------------------------------
  124. local parse_properties = function(t)
  125. local props = {}
  126. for _,v in ipairs(t) do
  127. if v._name == "property" then
  128. local attr = v._attr
  129. local ptype, pvalue = attr.type, attr.value
  130. if ptype == "bool" then
  131. if pvalue == "true" then pvalue = true
  132. elseif pvalue == "false" then pvalue = false
  133. else pvalue = nil end
  134. elseif ptype == "int" or ptype == "float" then
  135. pvalue = tonumber(pvalue)
  136. elseif ptype == "color" then
  137. if pvalue == "" then pvalue = {1, 1, 1, 1}
  138. else pvalue = parse_color(pvalue, true) end
  139. elseif ptype == "file" then
  140. pvalue = {
  141. path = pvalue,
  142. filepath = _concat(normalize_path(pvalue, Loader.npath), "/"),
  143. }
  144. end
  145. props[attr.name] = pvalue
  146. end
  147. end
  148. return props
  149. end
  150. local parse_tileset = function(t)
  151. local attr = t._attr
  152. local firstgid = tonumber(attr.firstgid)
  153. local tsx = attr.source
  154. if tsx then -- external tileset
  155. local path = _concat(normalize_path(tsx, Loader.npath), "/")
  156. t = xml.parse(love.filesystem.read(path))
  157. for _,v in ipairs(t) do
  158. if v._name == "tileset" then t = v break end
  159. end
  160. attr = t._attr
  161. attr.firstgid = firstgid
  162. --attr.source = tsx
  163. end
  164. local tileset = {
  165. firstgid = firstgid,
  166. --source = tsx,
  167. name = attr.name,
  168. tileWidth = tonumber(attr.tilewidth),
  169. tileHeight = tonumber(attr.tileheight),
  170. spacing = tonumber(attr.spacing) or 0,
  171. margin = tonumber(attr.margin) or 0,
  172. tileCount = tonumber(attr.tilecount),
  173. columns = tonumber(attr.columns),
  174. }
  175. local props
  176. local tileProperties = {}
  177. local tileTypes = {}
  178. local offsetX, offsetY = 0, 0
  179. local cached
  180. for _, v in ipairs(t) do
  181. local vname, vattr = v._name, v._attr
  182. if vname == "image" then
  183. assert(cached == nil, "multiple image sections not supported")
  184. local source = vattr.source
  185. local path = _concat(normalize_path(source, Loader.npath), "/")
  186. cached = cache[path]
  187. if not cached then
  188. local image = love.image.newImageData(path) -- exists?
  189. local trans = vattr.trans
  190. if trans then
  191. local R, G, B = parse_colorbytes(trans)
  192. local rb, gb, bb
  193. image:mapPixel( function(x, y, r, g, b, a)
  194. rb, gb, bb = colorToBytes(r, g, b)
  195. if R == rb and G == gb and B == bb then return r, g, b, 0 end
  196. return r,g,b,a
  197. end)
  198. trans = {colorFromBytes(R, G, B)}
  199. end
  200. local filter = Loader.filter
  201. image = love.graphics.newImage(image)
  202. image:setFilter(filter.min, filter.mag, filter.anisotropy)
  203. cached = {image = image, trans = trans, source = source}
  204. cached.width, cached.height = image:getDimensions()
  205. end
  206. cached.used = true
  207. cache[path] = cached
  208. elseif vname == "tile" then
  209. local tilegid = firstgid + vattr.id
  210. if vattr.type then tileTypes[tilegid] = vattr.type end
  211. for _, v2 in ipairs(v) do
  212. if v2._name == "properties" then
  213. tileProperties[tilegid] = parse_properties(v2)
  214. break
  215. end
  216. end
  217. elseif vname == "properties" then
  218. assert(cached == nil, "multiple properties sections not supported")
  219. props = parse_properties(v)
  220. elseif vname == "tileoffset" then
  221. assert(offsetX == nil, "multiple tileoffset sections not supported")
  222. offsetX, offsetY = vattr.x, vattr.y
  223. end
  224. end
  225. assert(cached, "parse_tileset - tileset does not contain an image")
  226. tileset.image = cached.image
  227. tileset.imageTrans = cached.trans
  228. tileset.imagePath = cached.source
  229. tileset.width = cached.width
  230. tileset.height = cached.height
  231. tileset.offsetX = tonumber(offsetX) or 0
  232. tileset.offsetY = tonumber(offsetY) or 0
  233. tileset.properties = props or {}
  234. tileset.tileProperties = tileProperties
  235. tileset.tileTypes = tileTypes
  236. return tileset
  237. end
  238. local parse_tilelayer_data = function(t)
  239. local data = {}
  240. local encoding = t._attr.encoding
  241. if encoding == nil then -- xml
  242. for k, v in ipairs(t) do
  243. if v._name == "tile" then
  244. _insert(data, tonumber(v._attr.gid) or 0)
  245. end
  246. end
  247. elseif encoding == "csv" then
  248. for s in _gmatch(t[1], "%d+") do
  249. _insert(data, tonumber(s))
  250. end
  251. elseif encoding == "base64" then
  252. local compfmt = t._attr.compression
  253. local decoded = love.data.decode("string", "base64", t[1])
  254. if compfmt == "gzip" or compfmt == "zlib" then
  255. decoded = love.data.decompress("string", compfmt, decoded)
  256. end
  257. local pos = 1
  258. for i = 1, math.floor(#decoded / 4) do
  259. data[i], pos = love.data.unpack("<I4", decoded, pos)
  260. end
  261. else
  262. --data = nil
  263. print("parse_tilelayer_data - unsupported encoding")
  264. end
  265. return data
  266. end
  267. local parse_tilelayer = function(t)
  268. local attr = t._attr
  269. local layer = {
  270. id = tonumber(attr.id),
  271. name = attr.name or ("Tile Layer " .. attr.id),
  272. width = tonumber(attr.width),
  273. height = tonumber(attr.height),
  274. opacity = tonumber(attr.opacity) or 1,
  275. visible = tonumber(attr.visible) ~= 0,
  276. offsetX = tonumber(attr.offsetx) or 0,
  277. offsetY = tonumber(attr.offsety) or 0,
  278. }
  279. local data, props
  280. for _, v in ipairs(t) do
  281. local vname = v._name
  282. if vname == "data" then
  283. assert(data == nil, "multiple data sections not supported")
  284. data = parse_tilelayer_data(v)
  285. elseif vname == "properties" then
  286. assert(props == nil, "multiple properties sections not supported")
  287. props = parse_properties(v)
  288. end
  289. end
  290. layer.properties = props or {}
  291. layer.data = data
  292. layer.type = "tilelayer"
  293. return layer
  294. end
  295. local parse_objectlayer = function(t)
  296. local attr = t._attr
  297. local layer = {
  298. id = tonumber(attr.id),
  299. name = attr.name or ("Object Layer " .. attr.id),
  300. color = parse_color(attr.color, true) or {0.5, 0.5, 0.5, 1},
  301. opacity = tonumber(attr.opacity) or 1,
  302. visible = tonumber(attr.visible) ~= 0,
  303. offsetX = tonumber(attr.offsetx) or 0,
  304. offsetY = tonumber(attr.offsety) or 0,
  305. drawOrder = attr.draworder or "topdown"
  306. }
  307. local objects = {}
  308. local props
  309. for _, v in ipairs(t) do
  310. if v._name == "object" then
  311. local vattr = v._attr
  312. local obj = {
  313. id = tonumber(vattr.id),
  314. name = vattr.name or "",
  315. type = vattr.type or "",
  316. x = tonumber(vattr.x),
  317. y = tonumber(vattr.y),
  318. width = tonumber(vattr.width) or 0,
  319. height = tonumber(vattr.height) or 0,
  320. rotation = tonumber(vattr.rotation) or 0,
  321. gid = tonumber(vattr.gid),
  322. visible = tonumber(vattr.visible) ~= 0,
  323. }
  324. _insert(objects, obj)
  325. for _, v2 in ipairs(v) do
  326. local v2name = v2._name
  327. if v2name == "properties" then
  328. obj.properties = parse_properties(v2)
  329. elseif v2name == "ellipse" then obj.ellipse = true
  330. elseif v2name == "point" then obj.point = true
  331. elseif v2name == "text" then obj.text = v2[1] or ""
  332. elseif v2name == "polyline" then
  333. obj.polyline = {}
  334. for num in _gmatch(v2._attr.points, "-?%d+") do
  335. _insert(obj.polyline, tonumber(num))
  336. end
  337. elseif v2name == "polygon" then
  338. obj.polygon = {}
  339. for num in _gmatch(v2._attr.points, "-?%d+") do
  340. _insert(obj.polygon, tonumber(num))
  341. end
  342. end
  343. end
  344. elseif v._name == "properties" then
  345. props = parse_properties(v)
  346. end
  347. end
  348. layer.properties = props or {}
  349. layer.objects = objects
  350. layer.type = "objectgroup"
  351. return layer
  352. end
  353. -- parse map
  354. local parse_map = function(t)
  355. local attr = t._attr
  356. local map = {
  357. version = attr.version,
  358. tiledVersion = attr.tiledversion,
  359. orientation = attr.orientation,
  360. renderOrder = attr.renderorder,
  361. width = tonumber(attr.width),
  362. height = tonumber(attr.height),
  363. tileWidth = tonumber(attr.tilewidth),
  364. tileHeight = tonumber(attr.tileheight),
  365. hexsidelength = tonumber(attr.hexsidelength),
  366. staggerAxis = attr.staggeraxis,
  367. staggerIndex = attr.staggerindex,
  368. backgroundColor = parse_color(attr.backgroundcolor, true),-- or {0, 0, 0, 1},
  369. infinite = tonumber(attr.infinite) == 1,
  370. nextLayerID = tonumber(attr.nextlayerid),
  371. nextObjectID = tonumber(attr.nextobjectid),
  372. }
  373. local props
  374. local tilesets, layers = {}, {}
  375. for _, v in ipairs(t) do
  376. local vname = v._name
  377. if vname == "properties" then
  378. assert(props == nil, "multiple properties sections not supported")
  379. props = parse_properties(v)
  380. elseif vname == "tileset" then
  381. local tileset = parse_tileset(v, map)
  382. tilesets[tileset.name] = tileset
  383. elseif vname == "layer" then
  384. _insert(layers, parse_tilelayer(v))
  385. elseif vname == "objectgroup" then
  386. _insert(layers, parse_objectlayer(v))
  387. end
  388. end
  389. map.properties = props or {}
  390. map.tilesets = tilesets
  391. map.layerOrder = layers
  392. return map
  393. end
  394. ----------------------------------------------------------------
  395. -- LOADS A TMX FILE
  396. ----------------------------------------------------------------
  397. local Map = require(PATH .. "Map")
  398. local TileSet = require(PATH .. "TileSet")
  399. local TileLayer = require(PATH .. "TileLayer")
  400. local Object = require(PATH .. "Object")
  401. local ObjectLayer = require(PATH .. "ObjectLayer")
  402. Loader.load = function(filepath)
  403. local npath = normalize_path(filepath)
  404. Loader.filename = _remove(npath)
  405. Loader.filedir = _concat(npath, "/")
  406. Loader.filepath = Loader.filedir .. "/" .. Loader.filename
  407. Loader.npath = npath
  408. filepath = Loader.filepath
  409. if not love.filesystem.getInfo(filepath, "file") then
  410. print("Loader.load - could not find the file: " .. filepath)
  411. return
  412. end
  413. local parsed = xml.parse(love.filesystem.read(filepath))
  414. local map
  415. for i, v in ipairs(parsed) do
  416. if v._name == "map" then
  417. invalidate(cache)
  418. map = parse_map(v)
  419. clear_unused(cache)
  420. break
  421. end
  422. end
  423. if not map then
  424. print("Loader.load - missing map section in file: " .. filepath)
  425. return
  426. end
  427. map.directory = Loader.filedir
  428. map.name = Loader.filename
  429. map.useSpriteBatch = Loader.useSpriteBatch
  430. map.drawObjects = Loader.drawObjects
  431. map.visible = true
  432. Map.init(map)
  433. for k, v in pairs(map.tilesets) do
  434. v.map = map
  435. TileSet.init(v)
  436. end
  437. map:updateTiles()
  438. map.layers = {}
  439. for i, v in ipairs(map.layerOrder) do
  440. v.map = map
  441. if v.type == "tilelayer" then
  442. v.useSpriteBatch = map.useSpriteBatch
  443. TileLayer.init(v)
  444. v:_populate(v.data)
  445. v.data = nil
  446. elseif v.type == "objectgroup" then
  447. ObjectLayer.init(v)
  448. for _, obj in ipairs(v.objects) do
  449. obj.layer = v
  450. Object.init(obj)
  451. end
  452. end
  453. map.layers[v.name] = v
  454. --map.layerOrder[#map.layerOrder + 1] = v
  455. end
  456. return map
  457. end
  458. ----------------------------------------------------------------
  459. -- SAVES A MAP AS TMX (REMOVED)
  460. ----------------------------------------------------------------
  461. local xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n'
  462. local defaultSavePath = "Saved Maps"
  463. -- save a tmx file
  464. function Loader.save(map, filename, path)
  465. path = path or defaultSavePath
  466. if not love.filesystem.getInfo(path, "directory") then
  467. love.filesystem.createDirectory(path)
  468. end
  469. --love.filesystem.write(path .. "/" .. filename,
  470. -- xmlHeader .. xml.toxml(write_map(map)) )
  471. end
  472. return Loader