From ff33a7538e871775f2f49a576f59f1e4f7fc4131 Mon Sep 17 00:00:00 2001 From: Alexander Griffith Date: Sat, 20 Apr 2019 20:38:53 -0400 Subject: [PATCH] Update for Spring 2019 Game Jam --- changelog.org | 4 - conf.lua | 2 +- lib/bump.lua | 773 ------------------ lib/fennel | 141 +++- lib/fennel.lua | 923 +++++++++++++++++---- lib/fennelview.lua | 161 ++-- lib/intersect.lua | 15 - lib/lume.lua | 54 +- lib/sfxr.lua | 1536 ----------------------------------- lib/tiled/graphics.lua | 126 --- lib/tiled/init.lua | 1485 --------------------------------- lib/tiled/plugins/box2d.lua | 286 ------- lib/tiled/plugins/bump.lua | 206 ----- lib/tiled/utils.lua | 208 ----- 14 files changed, 964 insertions(+), 4956 deletions(-) delete mode 100644 changelog.org delete mode 100644 lib/bump.lua delete mode 100644 lib/intersect.lua delete mode 100644 lib/sfxr.lua delete mode 100644 lib/tiled/graphics.lua delete mode 100644 lib/tiled/init.lua delete mode 100644 lib/tiled/plugins/box2d.lua delete mode 100644 lib/tiled/plugins/bump.lua delete mode 100644 lib/tiled/utils.lua diff --git a/changelog.org b/changelog.org deleted file mode 100644 index 1b7ceeb..0000000 --- a/changelog.org +++ /dev/null @@ -1,4 +0,0 @@ -* 2018-10-16 Alexander Griffith - - Updated love version in love-release.sh to 11.1 - - Added libs fennelview, lume & stdio - diff --git a/conf.lua b/conf.lua index 274ef20..4e50613 100644 --- a/conf.lua +++ b/conf.lua @@ -6,5 +6,5 @@ love.conf = function(t) t.window.width = 720 t.window.height = 450 t.window.vsync = false - t.version = "11.1" + t.version = "11.2" end diff --git a/lib/bump.lua b/lib/bump.lua deleted file mode 100644 index 6dabca7..0000000 --- a/lib/bump.lua +++ /dev/null @@ -1,773 +0,0 @@ -local bump = { - _VERSION = 'bump v3.1.7', - _URL = 'https://github.com/kikito/bump.lua', - _DESCRIPTION = 'A collision detection library for Lua', - _LICENSE = [[ - MIT LICENSE - - Copyright (c) 2014 Enrique García Cota - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be included - in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ]] -} - ------------------------------------------- --- Auxiliary functions ------------------------------------------- -local DELTA = 1e-10 -- floating-point margin of error - -local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max - -local function sign(x) - if x > 0 then return 1 end - if x == 0 then return 0 end - return -1 -end - -local function nearest(x, a, b) - if abs(a - x) < abs(b - x) then return a else return b end -end - -local function assertType(desiredType, value, name) - if type(value) ~= desiredType then - error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')') - end -end - -local function assertIsPositiveNumber(value, name) - if type(value) ~= 'number' or value <= 0 then - error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')') - end -end - -local function assertIsRect(x,y,w,h) - assertType('number', x, 'x') - assertType('number', y, 'y') - assertIsPositiveNumber(w, 'w') - assertIsPositiveNumber(h, 'h') -end - -local defaultFilter = function() - return 'slide' -end - ------------------------------------------- --- Rectangle functions ------------------------------------------- - -local function rect_getNearestCorner(x,y,w,h, px, py) - return nearest(px, x, x+w), nearest(py, y, y+h) -end - --- This is a generalized implementation of the liang-barsky algorithm, which also returns --- the normals of the sides where the segment intersects. --- Returns nil if the segment never touches the rect --- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge -local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2) - ti1, ti2 = ti1 or 0, ti2 or 1 - local dx, dy = x2-x1, y2-y1 - local nx, ny - local nx1, ny1, nx2, ny2 = 0,0,0,0 - local p, q, r - - for side = 1,4 do - if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left - elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right - elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top - else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom - end - - if p == 0 then - if q <= 0 then return nil end - else - r = q / p - if p < 0 then - if r > ti2 then return nil - elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny - end - else -- p > 0 - if r < ti1 then return nil - elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny - end - end - end - end - - return ti1,ti2, nx1,ny1, nx2,ny2 -end - --- Calculates the minkowsky difference between 2 rects, which is another rect -local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2) - return x2 - x1 - w1, - y2 - y1 - h1, - w1 + w2, - h1 + h2 -end - -local function rect_containsPoint(x,y,w,h, px,py) - return px - x > DELTA and py - y > DELTA and - x + w - px > DELTA and y + h - py > DELTA -end - -local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2) - return x1 < x2+w2 and x2 < x1+w1 and - y1 < y2+h2 and y2 < y1+h1 -end - -local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2) - local dx = x1 - x2 + (w1 - w2)/2 - local dy = y1 - y2 + (h1 - h2)/2 - return dx*dx + dy*dy -end - -local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY) - goalX = goalX or x1 - goalY = goalY or y1 - - local dx, dy = goalX - x1, goalY - y1 - local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2) - - local overlaps, ti, nx, ny - - if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other - local px, py = rect_getNearestCorner(x,y,w,h, 0, 0) - local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection - ti = -wi * hi -- ti is the negative area of intersection - overlaps = true - else - local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge) - - -- item tunnels into other - if ti1 - and ti1 < 1 - and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner - and (0 < ti1 + DELTA - or 0 == ti1 and ti2 > 0) - then - ti, nx, ny = ti1, nx1, ny1 - overlaps = false - end - end - - if not ti then return end - - local tx, ty - - if overlaps then - if dx == 0 and dy == 0 then - -- intersecting and not moving - use minimum displacement vector - local px, py = rect_getNearestCorner(x,y,w,h, 0,0) - if abs(px) < abs(py) then py = 0 else px = 0 end - nx, ny = sign(px), sign(py) - tx, ty = x1 + px, y1 + py - else - -- intersecting and moving - move in the opposite direction - local ti1, _ - ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1) - if not ti1 then return end - tx, ty = x1 + dx * ti1, y1 + dy * ti1 - end - else -- tunnel - tx, ty = x1 + dx * ti, y1 + dy * ti - end - - return { - overlaps = overlaps, - ti = ti, - move = {x = dx, y = dy}, - normal = {x = nx, y = ny}, - touch = {x = tx, y = ty}, - itemRect = {x = x1, y = y1, w = w1, h = h1}, - otherRect = {x = x2, y = y2, w = w2, h = h2} - } -end - ------------------------------------------- --- Grid functions ------------------------------------------- - -local function grid_toWorld(cellSize, cx, cy) - return (cx - 1)*cellSize, (cy-1)*cellSize -end - -local function grid_toCell(cellSize, x, y) - return floor(x / cellSize) + 1, floor(y / cellSize) + 1 -end - --- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing", --- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf --- It has been modified to include both cells when the ray "touches a grid corner", --- and with a different exit condition - -local function grid_traverse_initStep(cellSize, ct, t1, t2) - local v = t2 - t1 - if v > 0 then - return 1, cellSize / v, ((ct + v) * cellSize - t1) / v - elseif v < 0 then - return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v - else - return 0, math.huge, math.huge - end -end - -local function grid_traverse(cellSize, x1,y1,x2,y2, f) - local cx1,cy1 = grid_toCell(cellSize, x1,y1) - local cx2,cy2 = grid_toCell(cellSize, x2,y2) - local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2) - local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2) - local cx,cy = cx1,cy1 - - f(cx, cy) - - -- The default implementation had an infinite loop problem when - -- approaching the last cell in some occassions. We finish iterating - -- when we are *next* to the last cell - while abs(cx - cx2) + abs(cy - cy2) > 1 do - if tx < ty then - tx, cx = tx + dx, cx + stepX - f(cx, cy) - else - -- Addition: include both cells when going through corners - if tx == ty then f(cx + stepX, cy) end - ty, cy = ty + dy, cy + stepY - f(cx, cy) - end - end - - -- If we have not arrived to the last cell, use it - if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end - -end - -local function grid_toCellRect(cellSize, x,y,w,h) - local cx,cy = grid_toCell(cellSize, x, y) - local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize) - return cx, cy, cr - cx + 1, cb - cy + 1 -end - ------------------------------------------- --- Responses ------------------------------------------- - -local touch = function(world, col, x,y,w,h, goalX, goalY, filter) - return col.touch.x, col.touch.y, {}, 0 -end - -local cross = function(world, col, x,y,w,h, goalX, goalY, filter) - local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) - return goalX, goalY, cols, len -end - -local slide = function(world, col, x,y,w,h, goalX, goalY, filter) - goalX = goalX or x - goalY = goalY or y - - local tch, move = col.touch, col.move - if move.x ~= 0 or move.y ~= 0 then - if col.normal.x ~= 0 then - goalX = tch.x - else - goalY = tch.y - end - end - - col.slide = {x = goalX, y = goalY} - - x,y = tch.x, tch.y - local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) - return goalX, goalY, cols, len -end - -local bounce = function(world, col, x,y,w,h, goalX, goalY, filter) - goalX = goalX or x - goalY = goalY or y - - local tch, move = col.touch, col.move - local tx, ty = tch.x, tch.y - - local bx, by = tx, ty - - if move.x ~= 0 or move.y ~= 0 then - local bnx, bny = goalX - tx, goalY - ty - if col.normal.x == 0 then bny = -bny else bnx = -bnx end - bx, by = tx + bnx, ty + bny - end - - col.bounce = {x = bx, y = by} - x,y = tch.x, tch.y - goalX, goalY = bx, by - - local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) - return goalX, goalY, cols, len -end - ------------------------------------------- --- World ------------------------------------------- - -local World = {} -local World_mt = {__index = World} - --- Private functions and methods - -local function sortByWeight(a,b) return a.weight < b.weight end - -local function sortByTiAndDistance(a,b) - if a.ti == b.ti then - local ir, ar, br = a.itemRect, a.otherRect, b.otherRect - local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h) - local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h) - return ad < bd - end - return a.ti < b.ti -end - -local function addItemToCell(self, item, cx, cy) - self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'}) - local row = self.rows[cy] - row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})} - local cell = row[cx] - self.nonEmptyCells[cell] = true - if not cell.items[item] then - cell.items[item] = true - cell.itemCount = cell.itemCount + 1 - end -end - -local function removeItemFromCell(self, item, cx, cy) - local row = self.rows[cy] - if not row or not row[cx] or not row[cx].items[item] then return false end - - local cell = row[cx] - cell.items[item] = nil - cell.itemCount = cell.itemCount - 1 - if cell.itemCount == 0 then - self.nonEmptyCells[cell] = nil - end - return true -end - -local function getDictItemsInCellRect(self, cl,ct,cw,ch) - local items_dict = {} - for cy=ct,ct+ch-1 do - local row = self.rows[cy] - if row then - for cx=cl,cl+cw-1 do - local cell = row[cx] - if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling - for item,_ in pairs(cell.items) do - items_dict[item] = true - end - end - end - end - end - - return items_dict -end - -local function getCellsTouchedBySegment(self, x1,y1,x2,y2) - - local cells, cellsLen, visited = {}, 0, {} - - grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy) - local row = self.rows[cy] - if not row then return end - local cell = row[cx] - if not cell or visited[cell] then return end - - visited[cell] = true - cellsLen = cellsLen + 1 - cells[cellsLen] = cell - end) - - return cells, cellsLen -end - -local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter) - local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2) - local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1 - local visited, itemInfo, itemInfoLen = {},{},0 - for i=1,len do - cell = cells[i] - for item in pairs(cell.items) do - if not visited[item] then - visited[item] = true - if (not filter or filter(item)) then - rect = self.rects[item] - l,t,w,h = rect.x,rect.y,rect.w,rect.h - - ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1) - if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then - -- the sorting is according to the t of an infinite line, not the segment - tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge) - itemInfoLen = itemInfoLen + 1 - itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)} - end - end - end - end - end - table.sort(itemInfo, sortByWeight) - return itemInfo, itemInfoLen -end - -local function getResponseByName(self, name) - local response = self.responses[name] - if not response then - error(('Unknown collision type: %s (%s)'):format(name, type(name))) - end - return response -end - - --- Misc Public Methods - -function World:addResponse(name, response) - self.responses[name] = response -end - -function World:project(item, x,y,w,h, goalX, goalY, filter) - assertIsRect(x,y,w,h) - - goalX = goalX or x - goalY = goalY or y - filter = filter or defaultFilter - - local collisions, len = {}, 0 - - local visited = {} - if item ~= nil then visited[item] = true end - - -- This could probably be done with less cells using a polygon raster over the cells instead of a - -- bounding rect of the whole movement. Conditional to building a queryPolygon method - local tl, tt = min(goalX, x), min(goalY, y) - local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h) - local tw, th = tr-tl, tb-tt - - local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th) - - local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch) - - for other,_ in pairs(dictItemsInCellRect) do - if not visited[other] then - visited[other] = true - - local responseName = filter(item, other) - if responseName then - local ox,oy,ow,oh = self:getRect(other) - local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY) - - if col then - col.other = other - col.item = item - col.type = responseName - - len = len + 1 - collisions[len] = col - end - end - end - end - - table.sort(collisions, sortByTiAndDistance) - - return collisions, len -end - -function World:countCells() - local count = 0 - for _,row in pairs(self.rows) do - for _,_ in pairs(row) do - count = count + 1 - end - end - return count -end - -function World:hasItem(item) - return not not self.rects[item] -end - -function World:getItems() - local items, len = {}, 0 - for item,_ in pairs(self.rects) do - len = len + 1 - items[len] = item - end - return items, len -end - -function World:countItems() - local len = 0 - for _ in pairs(self.rects) do len = len + 1 end - return len -end - -function World:getRect(item) - local rect = self.rects[item] - if not rect then - error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.') - end - return rect.x, rect.y, rect.w, rect.h -end - -function World:toWorld(cx, cy) - return grid_toWorld(self.cellSize, cx, cy) -end - -function World:toCell(x,y) - return grid_toCell(self.cellSize, x, y) -end - - ---- Query methods - -function World:queryRect(x,y,w,h, filter) - - assertIsRect(x,y,w,h) - - local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) - local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch) - - local items, len = {}, 0 - - local rect - for item,_ in pairs(dictItemsInCellRect) do - rect = self.rects[item] - if (not filter or filter(item)) - and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h) - then - len = len + 1 - items[len] = item - end - end - - return items, len -end - -function World:queryPoint(x,y, filter) - local cx,cy = self:toCell(x,y) - local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1) - - local items, len = {}, 0 - - local rect - for item,_ in pairs(dictItemsInCellRect) do - rect = self.rects[item] - if (not filter or filter(item)) - and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y) - then - len = len + 1 - items[len] = item - end - end - - return items, len -end - -function World:querySegment(x1, y1, x2, y2, filter) - local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter) - local items = {} - for i=1, len do - items[i] = itemInfo[i].item - end - return items, len -end - -function World:querySegmentWithCoords(x1, y1, x2, y2, filter) - local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter) - local dx, dy = x2-x1, y2-y1 - local info, ti1, ti2 - for i=1, len do - info = itemInfo[i] - ti1 = info.ti1 - ti2 = info.ti2 - - info.weight = nil - info.x1 = x1 + dx * ti1 - info.y1 = y1 + dy * ti1 - info.x2 = x1 + dx * ti2 - info.y2 = y1 + dy * ti2 - end - return itemInfo, len -end - - ---- Main methods - -function World:add(item, x,y,w,h) - local rect = self.rects[item] - if rect then - error('Item ' .. tostring(item) .. ' added to the world twice.') - end - assertIsRect(x,y,w,h) - - self.rects[item] = {x=x,y=y,w=w,h=h} - - local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) - for cy = ct, ct+ch-1 do - for cx = cl, cl+cw-1 do - addItemToCell(self, item, cx, cy) - end - end - - return item -end - -function World:remove(item) - local x,y,w,h = self:getRect(item) - - self.rects[item] = nil - local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h) - for cy = ct, ct+ch-1 do - for cx = cl, cl+cw-1 do - removeItemFromCell(self, item, cx, cy) - end - end -end - -function World:update(item, x2,y2,w2,h2) - local x1,y1,w1,h1 = self:getRect(item) - w2,h2 = w2 or w1, h2 or h1 - assertIsRect(x2,y2,w2,h2) - - if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then - - local cellSize = self.cellSize - local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1) - local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2) - - if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then - - local cr1, cb1 = cl1+cw1-1, ct1+ch1-1 - local cr2, cb2 = cl2+cw2-1, ct2+ch2-1 - local cyOut - - for cy = ct1, cb1 do - cyOut = cy < ct2 or cy > cb2 - for cx = cl1, cr1 do - if cyOut or cx < cl2 or cx > cr2 then - removeItemFromCell(self, item, cx, cy) - end - end - end - - for cy = ct2, cb2 do - cyOut = cy < ct1 or cy > cb1 - for cx = cl2, cr2 do - if cyOut or cx < cl1 or cx > cr1 then - addItemToCell(self, item, cx, cy) - end - end - end - - end - - local rect = self.rects[item] - rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2 - - end -end - -function World:move(item, goalX, goalY, filter) - local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter) - - self:update(item, actualX, actualY) - - return actualX, actualY, cols, len -end - -function World:check(item, goalX, goalY, filter) - filter = filter or defaultFilter - - local visited = {[item] = true} - local visitedFilter = function(itm, other) - if visited[other] then return false end - return filter(itm, other) - end - - local cols, len = {}, 0 - - local x,y,w,h = self:getRect(item) - - local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter) - - while projected_len > 0 do - local col = projected_cols[1] - len = len + 1 - cols[len] = col - - visited[col.other] = true - - local response = getResponseByName(self, col.type) - - goalX, goalY, projected_cols, projected_len = response( - self, - col, - x, y, w, h, - goalX, goalY, - visitedFilter - ) - end - - return goalX, goalY, cols, len -end - - --- Public library functions - -bump.newWorld = function(cellSize) - cellSize = cellSize or 64 - assertIsPositiveNumber(cellSize, 'cellSize') - local world = setmetatable({ - cellSize = cellSize, - rects = {}, - rows = {}, - nonEmptyCells = {}, - responses = {} - }, World_mt) - - world:addResponse('touch', touch) - world:addResponse('cross', cross) - world:addResponse('slide', slide) - world:addResponse('bounce', bounce) - - return world -end - -bump.rect = { - getNearestCorner = rect_getNearestCorner, - getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices, - getDiff = rect_getDiff, - containsPoint = rect_containsPoint, - isIntersecting = rect_isIntersecting, - getSquareDistance = rect_getSquareDistance, - detectCollision = rect_detectCollision -} - -bump.responses = { - touch = touch, - cross = cross, - slide = slide, - bounce = bounce -} - -return bump diff --git a/lib/fennel b/lib/fennel index 8fd7652..8f61065 100755 --- a/lib/fennel +++ b/lib/fennel @@ -3,55 +3,132 @@ local fennel_dir = arg[0]:match("(.-)[^\\/]+$") package.path = fennel_dir .. "?.lua;" .. package.path local fennel = require('fennel') +local unpack = unpack or table.unpack local help = [[ Usage: fennel [FLAG] [FILE] - --repl : Launch an interactive repl session - --compile FILES : Compile files and write their Lua to stdout - --help : Display this text +Run fennel, a lisp programming language for the Lua runtime. - When not given a flag, runs the file given as the first argument.]] + --repl : Launch an interactive repl session + --compile FILES : Compile files and write their Lua to stdout + + --no-searcher : Skip installing package.searchers entry + --indent VAL : Indent compiler output with VAL + --add-package-path PATH : Add PATH to package.path for finding Lua modules + --add-fennel-path PATH : Add PATH to fennel.path for finding Fennel modules + --globals G1[,G2...] : Allow these globals in addition to standard ones + --globals-only G1[,G2] : Same as above, but exclude standard ones + + --help : Display this text + --version : Show version + + When not given a flag, runs the file given as the first argument. + When given neither flag nor file, launches a repl. + + If ~/.fennelrc exists, loads it before launching a repl.]] local options = { - sourcemap = true, + sourcemap = true } -local function dosafe(filename, opts, arg1) +local function dosafe(filename, opts, args) local ok, val = xpcall(function() - return fennel.dofile(filename, opts, arg1) + return fennel.dofile(filename, opts, unpack(args)) end, fennel.traceback) if not ok then - print(val) + io.stderr:write(val .. "\n") os.exit(1) end return val end -local compileOptHandlers = { - ['--indent'] = function () - options.indent = table.remove(arg, 3) +local function allowGlobals(globalNames) + options.allowedGlobals = {} + for g in globalNames:gmatch("([^,]+),?") do + table.insert(options.allowedGlobals, g) + end +end + +for i=#arg, 1, -1 do + if arg[i] == "--no-searcher" then + options.no_searcher = true + table.remove(arg, i) + elseif arg[i] == "--indent" then + options.indent = table.remove(arg, i+1) if options.indent == "false" then options.indent = false end - table.remove(arg, 2) - end, - ['--sourcemap'] = function () - options.sourcemap = table.remove(arg, 3) + table.remove(arg, i) + elseif arg[i] == "--add-package-path" then + local entry = table.remove(arg, i+1) + package.path = entry .. ";" .. package.path + table.remove(arg, i) + elseif arg[i] == "--add-fennel-path" then + local entry = table.remove(arg, i+1) + fennel.path = entry .. ";" .. fennel.path + table.remove(arg, i) + elseif arg[i] == "--sourcemap" then + options.sourcemap = table.remove(arg, i+1) if options.sourcemap == "false" then options.sourcemap = false end - table.remove(arg, 2) - end, - ['--correlate'] = function () - options.correlate = true - table.remove(arg, 2) - end, -} + table.remove(arg, i) + elseif arg[i] == "--globals" then + allowGlobals(table.remove(arg, i+1)) + for globalName in pairs(_G) do + table.insert(options.allowedGlobals, globalName) + end + table.remove(arg, i) + elseif arg[i] == "--globals-only" then + allowGlobals(table.remove(arg, i+1)) + table.remove(arg, i) + end +end + +if not options.no_searcher then + table.insert((package.loaders or package.searchers), + fennel.make_searcher({correlate = true})) +end + +-- Try to load readline library +local function tryReadline(opts) + local ok, readline = pcall(require, "readline") + if ok then + readline.set_options({ + keeplines = 1000 + }) + function opts.readChunk(parserState) + local prompt = parserState.stackSize > 0 and '.. ' or '>> ' + local str = readline.readline(prompt) + if str then + return str .. "\n" + end + end + + -- completer is registered by the repl, until then returns empty list + local completer + function opts.registerCompleter(replCompleter) + completer = replCompleter + end + local function replCompleter(text, from, to) + if completer then return completer(text, from, to) else return {} end + end + readline.set_complete_function(replCompleter) + end +end if arg[1] == "--repl" or #arg == 0 then local ppok, pp = pcall(fennel.dofile, fennel_dir .. "fennelview.fnl", options) if ppok then options.pp = pp + else + ppok, pp = pcall(require, "fennelview") + if ppok then + options.pp = pp + end end local initFilename = (os.getenv("HOME") or "") .. "/.fennelrc" local init = io.open(initFilename, "rb") + + tryReadline(options) + if init then init:close() -- pass in options so fennerlrc can make changes to it @@ -60,23 +137,25 @@ if arg[1] == "--repl" or #arg == 0 then print("Welcome to fennel!") fennel.repl(options) elseif arg[1] == "--compile" then - -- Handle options - while compileOptHandlers[arg[2]] do - compileOptHandlers[arg[2]]() - end for i = 2, #arg do - local f = assert(io.open(arg[i], "rb")) + local f = arg[i] == "-" and io.stdin or assert(io.open(arg[i], "rb")) options.filename=arg[i] local ok, val = xpcall(function() return fennel.compileString(f:read("*all"), options) end, fennel.traceback) - print(val) - if not ok then os.exit(1) end + if ok then + print(val) + else + io.stderr:write(val .. "\n") + os.exit(1) + end f:close() end -elseif #arg >= 1 and arg[1] ~= "--help" then +elseif arg[1] == "--version" or arg[1] == "-v" then + print("Fennel " .. fennel.version) +elseif #arg >= 1 and arg[1] ~= "--help" and arg[1] ~= "-h" then local filename = table.remove(arg, 1) -- let the script have remaining args - dosafe(filename) + dosafe(filename, nil, arg) else print(help) end diff --git a/lib/fennel.lua b/lib/fennel.lua index 8b6d96a..aca7d64 100644 --- a/lib/fennel.lua +++ b/lib/fennel.lua @@ -44,6 +44,7 @@ local LIST_MT = { 'LIST', return '(' .. table.concat(strs, ', ', 1, #self) .. ')' end } +local SEQUENCE_MT = { 'SEQUENCE' } -- Load code with an environment in all recent Lua versions local function loadCode(code, environment, filename) @@ -73,6 +74,11 @@ local function sym(str, scope, meta) return setmetatable(s, SYMBOL_MT) end +-- Create a new sequence +local function sequence(...) + return setmetatable({...}, SEQUENCE_MT) +end + -- Create a new expr -- etype should be one of -- "literal", -- literals like numbers, strings, nil, true, false @@ -84,6 +90,7 @@ local function expr(strcode, etype) return setmetatable({ strcode, type = etype }, EXPR_MT) end + local function varg() return VARARG end @@ -109,6 +116,11 @@ local function isTable(x) getmetatable(x) ~= LIST_MT and getmetatable(x) ~= SYMBOL_MT and x end +-- Checks if an object is a sequence (created with a [] literal) +local function isSequence(x) + return type(x) == 'table' and getmetatable(x) == SEQUENCE_MT and x +end + -- -- Parser -- @@ -119,14 +131,14 @@ local function granulate(getchunk) local c = '' local index = 1 local done = false - return function () + return function (parserState) if done then return nil end if index <= #c then local b = c:byte(index) index = index + 1 return b else - c = getchunk() + c = getchunk(parserState) if not c or c == '' then done = true return nil @@ -168,13 +180,19 @@ end local function issymbolchar(b) return b > 32 and not delims[b] and - b ~= 127 and - b ~= 34 and - b ~= 39 and - b ~= 59 and - b ~= 44 + b ~= 127 and -- "" + b ~= 34 and -- "\"" + b ~= 39 and -- "'" + b ~= 59 and -- ";" + b ~= 44 and -- "," + b ~= 96 -- "`" end +local prefixes = { -- prefix chars substituted while reading + [96] = 'quote', -- ` + [64] = 'unquote' -- @ +} + -- Parse one value given a function that -- returns sequential bytes. Will throw an error as soon -- as possible without getting more bytes on bad input. Returns @@ -200,15 +218,18 @@ local function parser(getbyte, filename) if lastb then r, lastb = lastb, nil else - r = getbyte() + r = getbyte({ stackSize = #stack }) end byteindex = byteindex + 1 if r == 10 then line = line + 1 end return r end + local function parseError(msg) + return error(msg .. ' in ' .. (filename or 'unknown') .. ':' .. line, 0) + end -- Parse stream - return function () + return function() -- Dispatch when we complete a value local done, retval @@ -216,11 +237,27 @@ local function parser(getbyte, filename) if #stack == 0 then retval = v done = true + elseif stack[#stack].prefix then + local stacktop = stack[#stack] + stack[#stack] = nil + return dispatch(list(sym(stacktop.prefix), v)) else table.insert(stack[#stack], v) end end + -- Throw nice error when we expect more characters + -- but reach end of stream. + local function badend() + local accum = {} + for _, item in ipairs(stack) do + accum[#accum + 1] = item.closer + end + parseError(('expected closing delimiter%s %s'):format( + #stack == 1 and "" or "s", + string.char(unpack(accum)))) + end + -- The main parse loop repeat local b @@ -230,7 +267,7 @@ local function parser(getbyte, filename) b = getb() until not b or not iswhitespace(b) if not b then - if #stack > 0 then error 'unexpected end of source' end + if #stack > 0 then badend() end return nil end @@ -246,23 +283,24 @@ local function parser(getbyte, filename) bytestart = byteindex }, LIST_MT)) elseif delims[b] then -- Closing delimiter - if #stack == 0 then error 'unexpected closing delimiter' end + if #stack == 0 then parseError 'unexpected closing delimiter' end local last = stack[#stack] local val if last.closer ~= b then - error('unexpected delimiter ' .. string.char(b) .. ', expected ' .. string.char(last.closer)) + parseError('unexpected delimiter ' .. string.char(b) .. + ', expected ' .. string.char(last.closer)) end last.byteend = byteindex -- Set closing byte index - if b == 41 then -- ) + if b == 41 then -- ; ) val = last - elseif b == 93 then -- ] - val = {} + elseif b == 93 then -- ; ] + val = sequence() for i = 1, #last do val[i] = last[i] end - else -- } + else -- ; } if #last % 2 ~= 0 then - error 'expected even number of values in table literal' + parseError('expected even number of values in table literal') end val = {} for i = 1, #last, 2 do @@ -275,6 +313,7 @@ local function parser(getbyte, filename) local start = b local state = "base" local chars = {start} + stack[#stack + 1] = {closer = b} repeat b = getb() chars[#chars + 1] = b @@ -289,10 +328,16 @@ local function parser(getbyte, filename) state = "base" end until not b or (state == "done") - if not b then error 'unexpected end of source' end + if not b then badend() end + stack[#stack] = nil local raw = string.char(unpack(chars)) - local loadFn = loadCode(('return %s'):format(raw), nil, filename) + local formatted = raw:gsub("[\1-\31]", function (c) return '\\' .. c:byte() end) + local loadFn = loadCode(('return %s'):format(formatted), nil, filename) dispatch(loadFn()) + elseif prefixes[b] then -- expand prefix byte into wrapping form eg. '`a' into '(quote a)' + table.insert(stack, { + prefix = prefixes[b] + }) else -- Try symbol local chars = {} local bytestart = byteindex @@ -309,22 +354,25 @@ local function parser(getbyte, filename) dispatch(rawstr:sub(2)) else local forceNumber = rawstr:match('^%d') + local numberWithStrippedUnderscores = rawstr:gsub("_", "") local x if forceNumber then - x = tonumber(rawstr) or error('could not read token "' .. rawstr .. '"') + x = tonumber(numberWithStrippedUnderscores) or + parseError('could not read token "' .. rawstr .. '"') else - x = tonumber(rawstr) or sym(rawstr, nil, { - line = line, - filename = filename, - bytestart = bytestart, - byteend = byteindex - }) + x = tonumber(numberWithStrippedUnderscores) or + sym(rawstr, nil, { line = line, + filename = filename, + bytestart = bytestart, + byteend = byteindex, }) end dispatch(x) end end until done return true, retval + end, function () + stack = {} end end @@ -361,13 +409,15 @@ end local function assertCompile(condition, msg, ast) -- if we use regular `assert' we can't provide the `level' argument of zero if not condition then - error(string.format("Compile error in '%s' %s:%s: %s", ast[1][1], + error(string.format("Compile error in '%s' %s:%s: %s", + isSym(ast[1]) and ast[1][1] or ast[1] or '()', ast.filename or "unknown", ast.line or '?', msg), 0) end return condition end local GLOBAL_SCOPE = makeScope() +GLOBAL_SCOPE.vararg = true local SPECIALS = GLOBAL_SCOPE.specials local COMPILER_SCOPE = makeScope(GLOBAL_SCOPE) @@ -448,13 +498,13 @@ end -- Creates a symbol from a string by mangling it. -- ensures that the generated symbol is unique -- if the input string is unique in the scope. -local function localMangling(str, scope) +local function localMangling(str, scope, ast) if scope.manglings[str] then return scope.manglings[str] end local append = 0 local mangling = str - if isMultiSym(str) then error 'did not expect a multi symbol' end + assertCompile(not isMultiSym(str), 'did not expect multi symbol ' .. str, ast) -- Mapping mangling to a valid Lua identifier if luaKeywords[mangling] or mangling:match('^%d') then @@ -500,21 +550,47 @@ local function gensym(scope) return mangling end +-- Check if a binding is valid +local function checkBindingValid(symbol, scope, ast) + -- Check if symbol will be over shadowed by special + local name = symbol[1] + assertCompile(not scope.specials[name], + ("symbol %s may be overshadowed by a special form or macro"):format(name), ast) +end + -- Declare a local symbol local function declareLocal(symbol, meta, scope, ast) + checkBindingValid(symbol, scope, ast) local name = symbol[1] assertCompile(not isMultiSym(name), "did not expect mutltisym", ast) - local mangling = localMangling(name, scope) + local mangling = localMangling(name, scope, ast) scope.symmeta[name] = meta return mangling end +-- If there's a provided list of allowed globals, don't let references +-- thru that aren't on the list. This list is set at the compiler +-- entry points of compile and compileStream. +local allowedGlobals + +local function globalAllowed(name) + if not allowedGlobals then return true end + for _, g in ipairs(allowedGlobals) do + if g == name then return true end + end +end + -- Convert symbol to Lua code. Will only work for local symbols -- if they have already been declared via declareLocal -local function symbolToExpression(symbol, scope) +local function symbolToExpression(symbol, scope, isReference) local name = symbol[1] local parts = isMultiSym(name) or {name} local etype = (#parts > 1) and "expression" or "sym" + local isLocal = scope.manglings[parts[1]] + -- if it's a reference and not a symbol which introduces a new binding + -- then we need to check for allowed globals + assertCompile(not isReference or isLocal or globalAllowed(parts[1]), + 'unknown global in strict mode: ' .. parts[1], symbol) return expr(combineParts(parts, scope), etype) end @@ -675,7 +751,7 @@ local function handleCompileOpts(exprs, parent, opts, ast) if len > n then -- Drop extra keepSideEffects(exprs, parent, n + 1, ast) - for i = n, len do + for i = n + 1, len do exprs[i] = nil end else @@ -719,6 +795,7 @@ end -- Could be one variable, 'a', or a list, like 'a, b, _0_'. -- 'tail' - boolean indicating tail position if set. If set, form will generate a return -- instruction. +-- 'nval' - The number of values to compile to if it is known to be a fixed value. local function compile1(ast, scope, parent, opts) opts = opts or {} local exprs = {} @@ -727,7 +804,7 @@ local function compile1(ast, scope, parent, opts) if isList(ast) then -- Function call or special form local len = #ast - assert(len > 0, "expected a function to call") + assertCompile(len > 0, "expected a function to call", ast) -- Test for special form local first = ast[1] if isSym(first) then -- Resolve symbol @@ -742,7 +819,7 @@ local function compile1(ast, scope, parent, opts) if type(exprs) == 'string' then exprs = expr(exprs, 'expression') end if getmetatable(exprs) == EXPR_MT then exprs = {exprs} end -- Unless the special form explicitly handles the target, tail, and nval properties, - -- (indicated via the 'returned' flag, handle these options. + -- (indicated via the 'returned' flag), handle these options. if not exprs.returned then exprs = handleCompileOpts(exprs, parent, opts, ast) elseif opts.tail or opts.target then @@ -756,7 +833,8 @@ local function compile1(ast, scope, parent, opts) local fcallee = compile1(ast[1], scope, parent, { nval = 1 })[1] - assert(fcallee.type ~= 'literal', 'cannot call literal value') + assertCompile(fcallee.type ~= 'literal', + 'cannot call literal value', ast) fcallee = tostring(fcallee) for i = 2, len do local subexprs = compile1(ast[i], scope, parent, { @@ -777,7 +855,7 @@ local function compile1(ast, scope, parent, opts) exprs = handleCompileOpts({expr(call, 'statement')}, parent, opts, ast) end elseif isVarg(ast) then - -- assertCompile(scope.vararg, "unexpected vararg", ast) + assertCompile(scope.vararg, "unexpected vararg", ast) exprs = handleCompileOpts({expr('...', 'varg')}, parent, opts, ast) elseif isSym(ast) then local e @@ -786,7 +864,7 @@ local function compile1(ast, scope, parent, opts) if ast[1] == 'nil' then e = expr('nil', 'literal') else - e = symbolToExpression(ast, scope) + e = symbolToExpression(ast, scope, true) end exprs = handleCompileOpts({e}, parent, opts, ast) elseif type(ast) == 'nil' or type(ast) == 'boolean' then @@ -800,7 +878,8 @@ local function compile1(ast, scope, parent, opts) elseif type(ast) == 'table' then local buffer = {} for i = 1, #ast do -- Write numeric keyed values. - buffer[#buffer + 1] = tostring(compile1(ast[i], scope, parent, {nval = 1})[1]) + local nval = i ~= #ast and 1 + buffer[#buffer + 1] = exprs1(compile1(ast[i], scope, parent, {nval = nval})) end local keys = {} for k, _ in pairs(ast) do -- Write other keys. @@ -820,10 +899,10 @@ local function compile1(ast, scope, parent, opts) buffer[#buffer + 1] = ('%s = %s'):format( k[1], tostring(compile1(v, scope, parent, {nval = 1})[1])) end - local tbl = '({' .. table.concat(buffer, ', ') ..'})' + local tbl = '{' .. table.concat(buffer, ', ') ..'}' exprs = handleCompileOpts({expr(tbl, 'expression')}, parent, opts, ast) else - error('could not compile value of type ' .. type(ast)) + assertCompile(false, 'could not compile value of type ' .. type(ast), ast) end exprs.returned = true return exprs @@ -857,6 +936,7 @@ local function destructure(to, from, ast, scope, parent, opts) local nomulti = opts.nomulti local noundef = opts.noundef local forceglobal = opts.forceglobal + local forceset = opts.forceset local setter = declaration and "local %s = %s" or "%s = %s" -- Get Lua source for symbol, and check for errors @@ -869,7 +949,7 @@ local function destructure(to, from, ast, scope, parent, opts) else local parts = isMultiSym(raw) or {raw} local meta = scope.symmeta[parts[1]] - if #parts == 1 then + if #parts == 1 and not forceset then assertCompile(not(forceglobal and meta), 'expected global, found var', up1) assertCompile(meta or not noundef, @@ -881,23 +961,54 @@ local function destructure(to, from, ast, scope, parent, opts) end end + -- Compile the outer most form. We can generate better Lua in this case. + local function compileTopTarget(lvalues) + -- Calculate initial rvalue + local inits = {} + for _, x in ipairs(lvalues) do + table.insert(inits, scope.manglings[x] and x or 'nil') + end + local init = table.concat(inits, ', ') + local lvalue = table.concat(lvalues, ', ') + + local plen = #parent + local ret = compile1(from, scope, parent, {target = lvalue}) + if declaration then + if #parent == plen + 1 and parent[#parent].leaf then + -- A single leaf emitted means an simple assignment a = x was emitted + parent[#parent].leaf = 'local ' .. parent[#parent].leaf + else + table.insert(parent, plen + 1, { leaf = 'local ' .. lvalue .. ' = ' .. init, ast = ast}) + end + end + return ret + end + -- Recursive auxiliary function - local function destructure1(left, rightexprs, up1) + local function destructure1(left, rightexprs, up1, top) if isSym(left) and left[1] ~= "nil" then - emit(parent, setter:format(getname(left, up1), exprs1(rightexprs)), left) + checkBindingValid(left, scope, left) + local lname = getname(left, up1) + if top then + compileTopTarget({lname}) + else + emit(parent, setter:format(lname, exprs1(rightexprs)), left) + end elseif isTable(left) then -- table destructuring + if top then rightexprs = compile1(from, scope, parent) end local s = gensym(scope) emit(parent, ("local %s = %s"):format(s, exprs1(rightexprs)), left) - for i, v in ipairs(left) do - if isSym(left[i]) and left[i][1] == "&" then - assertCompile(not left[i+2], + for k, v in pairs(left) do + if isSym(left[k]) and left[k][1] == "&" then + assertCompile(type(k) == "number" and not left[k+2], "expected rest argument in final position", left) - local subexpr = expr(('{(table.unpack or unpack)(%s, %s)}'):format(s, i), + local subexpr = expr(('{(table.unpack or unpack)(%s, %s)}'):format(s, k), 'expression') - destructure1(left[i+1], {subexpr}, left) + destructure1(left[k+1], {subexpr}, left) return else - local subexpr = expr(('%s[%d]'):format(s, i), 'expression') + if type(k) ~= "number" then k = serializeString(k) end + local subexpr = expr(('%s[%s]'):format(s, k), 'expression') destructure1(v, {subexpr}, left) end end @@ -913,38 +1024,40 @@ local function destructure(to, from, ast, scope, parent, opts) end table.insert(leftNames, symname) end - emit(parent, setter: - format(table.concat(leftNames, ", "), exprs1(rightexprs)), left) + if top then + compileTopTarget(leftNames) + else + local lvalue = table.concat(leftNames, ', ') + emit(parent, setter:format(lvalue, exprs1(rightexprs)), left) + end for _, pair in pairs(tables) do -- recurse if left-side tables found destructure1(pair[1], {pair[2]}, left) end else assertCompile(false, 'unable to destructure ' .. tostring(left), up1) end + if top then return {returned = true} end end - local rexps = compile1(from, scope, parent) - local ret = destructure1(to, rexps, ast) - return ret + return destructure1(to, nil, ast, true) end -- Unlike most expressions and specials, 'values' resolves with multiple -- values, one for each argument, allowing multiple return values. The last --- expression, can return multiple arguments as well, allowing for more than the number +-- expression can return multiple arguments as well, allowing for more than the number -- of expected arguments. local function values(ast, scope, parent) local len = #ast local exprs = {} for i = 2, len do - local subexprs = compile1(ast[i], scope, parent, {}) - exprs[#exprs + 1] = subexprs[1] or expr('nil', 'literal') + local subexprs = compile1(ast[i], scope, parent, { + nval = (i ~= len) and 1 + }) + exprs[#exprs + 1] = subexprs[1] if i == len then for j = 2, #subexprs do exprs[#exprs + 1] = subexprs[j] end - else - -- Emit sub expression only for side effects - keepSideEffects(subexprs, parent, 2, ast) end end return exprs @@ -989,8 +1102,9 @@ local function doImpl(ast, scope, parent, opts, start, chunk, subScope) else -- We will use an IIFE for the do local fname = gensym(scope) - emit(parent, ('local function %s()'):format(fname), ast) - retexprs = expr(fname .. '()', 'statement') + local fargs = scope.vararg and '...' or '' + emit(parent, ('local function %s(%s)'):format(fname, fargs), ast) + retexprs = expr(fname .. '(' .. fargs .. ')', 'statement') outerTail = true outerTarget = nil end @@ -1035,6 +1149,7 @@ SPECIALS['fn'] = function(ast, scope, parent) local index = 2 local fnName = isSym(ast[index]) local isLocalFn + fScope.vararg = false if fnName and fnName[1] ~= 'nil' then isLocalFn = not isMultiSym(fnName[1]) if isLocalFn then @@ -1055,10 +1170,14 @@ SPECIALS['fn'] = function(ast, scope, parent) assertCompile(i == #argList, "expected vararg in last parameter position", ast) argNameList[i] = '...' fScope.vararg = true - elseif isSym(argList[i]) - and argList[i][1] ~= "nil" - and not isMultiSym(argList[i][1]) then + elseif(isSym(argList[i]) and argList[i][1] ~= "nil" + and not isMultiSym(argList[i][1])) then argNameList[i] = declareLocal(argList[i], {}, fScope, ast) + elseif isTable(argList[i]) then + local raw = sym(gensym(scope)) + argNameList[i] = declareLocal(raw, {}, fScope, ast) + destructure(argList[i], raw, ast, fScope, fChunk, + { declaration = true, nomulti = true }) else assertCompile(false, 'expected symbol for function parameter', ast) end @@ -1078,15 +1197,21 @@ SPECIALS['fn'] = function(ast, scope, parent) end emit(parent, fChunk, ast) emit(parent, 'end', ast) - return fnName + return expr(fnName, 'sym') end -SPECIALS['luaexpr'] = function(ast) - return tostring(ast[2]) -end - -SPECIALS['luastatement'] = function(ast) - return expr(tostring(ast[2]), 'statement') +-- (lua "print('hello!')") -> prints hello, evaluates to nil +-- (lua "print 'hello!'" "10") -> prints hello, evaluates to the number 10 +-- (lua nil "{1,2,3}") -> Evaluates to a table literal +SPECIALS['lua'] = function(ast, _, parent) + assertCompile(#ast == 2 or #ast == 3, + "expected 2 or 3 arguments in 'lua' special form", ast) + if ast[2] ~= nil then + table.insert(parent, {leaf = tostring(ast[2]), ast = ast}) + end + if #ast == 3 then + return tostring(ast[3]) + end end -- Wrapper for table access @@ -1107,12 +1232,18 @@ SPECIALS['.'] = function(ast, scope, parent) table.insert(indices, '[' .. tostring(index) .. ']') end end - return tostring(lhs[1]) .. table.concat(indices) + -- extra parens are needed for table literals + if isTable(ast[2]) then + return '(' .. tostring(lhs[1]) .. ')' .. table.concat(indices) + else + return tostring(lhs[1]) .. table.concat(indices) + end end end SPECIALS['global'] = function(ast, scope, parent) assertCompile(#ast == 3, "expected name and value", ast) + if allowedGlobals then table.insert(allowedGlobals, ast[2][1]) end destructure(ast[2], ast[3], ast, scope, parent, { nomulti = true, forceglobal = true @@ -1126,6 +1257,13 @@ SPECIALS['set'] = function(ast, scope, parent) }) end +SPECIALS['set-forcibly!'] = function(ast, scope, parent) + assertCompile(#ast == 3, "expected name and value", ast) + destructure(ast[2], ast[3], ast, scope, parent, { + forceset = true + }) +end + SPECIALS['local'] = function(ast, scope, parent) assertCompile(#ast == 3, "expected name and value", ast) destructure(ast[2], ast[3], ast, scope, parent, { @@ -1163,6 +1301,7 @@ end -- For setting items in a table SPECIALS['tset'] = function(ast, scope, parent) + assertCompile(#ast > 3, ('tset form needs table, key, and value'), ast) local root = compile1(ast[2], scope, parent, {nval = 1})[1] local keys = {} for i = 3, #ast - 1 do @@ -1170,9 +1309,11 @@ SPECIALS['tset'] = function(ast, scope, parent) keys[#keys + 1] = tostring(key) end local value = compile1(ast[#ast], scope, parent, {nval = 1})[1] - emit(parent, ('%s[%s] = %s'):format(tostring(root), - table.concat(keys, ']['), - tostring(value)), ast) + local rootstr = tostring(root) + local fmtstr = (rootstr:match('^{')) and '(%s)[%s] = %s' or '%s[%s] = %s' + emit(parent, fmtstr:format(tostring(root), + table.concat(keys, ']['), + tostring(value)), ast) end -- The if special form behaves like the cond form in @@ -1183,22 +1324,42 @@ SPECIALS['if'] = function(ast, scope, parent, opts) local elseBranch = nil -- Calculate some external stuff. Optimizes for tail calls and what not - local outerTail = true - local outerTarget = nil - local wrapper = 'iife' - if opts.tail then - wrapper = 'none' + local wrapper, innerTail, innerTarget, targetExprs + if opts.tail or opts.target or opts.nval then + if opts.nval and opts.nval ~= 0 and not opts.target then + -- We need to create a target + targetExprs = {} + local accum = {} + for i = 1, opts.nval do + local s = gensym(scope) + accum[i] = s + targetExprs[i] = expr(s, 'sym') + end + wrapper = 'target' + innerTail = opts.tail + innerTarget = table.concat(accum, ', ') + else + wrapper = 'none' + innerTail = opts.tail + innerTarget = opts.target + end + else + wrapper = 'iife' + innerTail = true + innerTarget = nil end -- Compile bodies and conditions local bodyOpts = { - tail = outerTail, - target = outerTarget + tail = innerTail, + target = innerTarget, + nval = opts.nval } local function compileBody(i) local chunk = {} local cscope = makeScope(doScope) - compile1(ast[i], cscope, chunk, bodyOpts) + keepSideEffects(compile1(ast[i], cscope, chunk, bodyOpts), + chunk, nil, ast[i]) return { chunk = chunk, scope = cscope @@ -1206,7 +1367,9 @@ SPECIALS['if'] = function(ast, scope, parent, opts) end for i = 2, #ast - 1, 2 do local condchunk = {} - local cond = compile1(ast[i], doScope, condchunk, {nval = 1}) + local res = compile1(ast[i], doScope, condchunk, {nval = 1}) + local cond = res[1] + --print(ast[i], res, cond) local branch = compileBody(i + 1) branch.cond = cond branch.condchunk = condchunk @@ -1223,7 +1386,7 @@ SPECIALS['if'] = function(ast, scope, parent, opts) for i = 1, #branches do local branch = branches[i] local fstr = not branch.nested and 'if %s then' or 'elseif %s then' - local condLine = fstr:format(tostring(branch.cond[1])) + local condLine = fstr:format(tostring(branch.cond)) if branch.nested then emit(lastBuffer, branch.condchunk, ast) else @@ -1258,6 +1421,12 @@ SPECIALS['if'] = function(ast, scope, parent, opts) emit(parent, buffer[i], ast) end return {returned = true} + else -- wrapper == 'target' + emit(parent, ('local %s'):format(innerTarget), ast) + for i = 1, #buffer do + emit(parent, buffer[i], ast) + end + return targetExprs end end @@ -1266,14 +1435,26 @@ SPECIALS['each'] = function(ast, scope, parent) local binding = assertCompile(isTable(ast[2]), 'expected binding table', ast) local iter = table.remove(binding, #binding) -- last item is iterator call local bindVars = {} + local destructures = {} for _, v in ipairs(binding) do - assertCompile(isSym(v), 'expected iterator symbol', ast) - table.insert(bindVars, declareLocal(v, {}, scope, ast)) + assertCompile(isSym(v) or isTable(v), + 'expected iterator symbol or table', ast) + if(isSym(v)) then + table.insert(bindVars, declareLocal(v, {}, scope, ast)) + else + local raw = sym(gensym(scope)) + destructures[raw] = v + table.insert(bindVars, declareLocal(raw, {}, scope, ast)) + end end emit(parent, ('for %s in %s do'):format( table.concat(bindVars, ', '), tostring(compile1(iter, scope, parent, {nval = 1})[1])), ast) local chunk = {} + for raw, args in pairs(destructures) do + destructure(args, raw, ast, scope, chunk, + { declaration = true, nomulti = true }) + end compileDo(ast, scope, chunk, 3) emit(parent, chunk, ast) emit(parent, 'end', ast) @@ -1362,12 +1543,13 @@ SPECIALS[':'] = function(ast, scope, parent) table.concat(args, ', ')), 'statement') end -local function defineArithmeticSpecial(name, unaryPrefix) +local function defineArithmeticSpecial(name, zeroArity, unaryPrefix) local paddedOp = ' ' .. name .. ' ' SPECIALS[name] = function(ast, scope, parent) local len = #ast if len == 1 then - return unaryPrefix or '0' + assertCompile(zeroArity ~= nil, 'Expected more than 0 arguments', ast) + return expr(zeroArity, 'literal') else local operands = {} for i = 2, len do @@ -1378,8 +1560,12 @@ local function defineArithmeticSpecial(name, unaryPrefix) operands[#operands + 1] = tostring(subexprs[j]) end end - if #operands == 1 and unaryPrefix then - return '(' .. unaryPrefix .. paddedOp .. operands[1] .. ')' + if #operands == 1 then + if unaryPrefix then + return '(' .. unaryPrefix .. paddedOp .. operands[1] .. ')' + else + return operands[1] + end else return '(' .. table.concat(operands, paddedOp) .. ')' end @@ -1387,33 +1573,37 @@ local function defineArithmeticSpecial(name, unaryPrefix) end end -defineArithmeticSpecial('+') -defineArithmeticSpecial('..') +defineArithmeticSpecial('+', '0') +defineArithmeticSpecial('..', "''") defineArithmeticSpecial('^') -defineArithmeticSpecial('-', '') -defineArithmeticSpecial('*') +defineArithmeticSpecial('-', nil, '') +defineArithmeticSpecial('*', '1') defineArithmeticSpecial('%') -defineArithmeticSpecial('/', 1) -defineArithmeticSpecial('//', 1) -defineArithmeticSpecial('or') -defineArithmeticSpecial('and') +defineArithmeticSpecial('/', nil, '1') +defineArithmeticSpecial('//', nil, '1') +defineArithmeticSpecial('or', 'false') +defineArithmeticSpecial('and', 'true') -local function defineComparatorSpecial(name, realop) +local function defineComparatorSpecial(name, realop, chainOp) local op = realop or name SPECIALS[name] = function(ast, scope, parent) - assertCompile(#ast > 2, 'expected at least two arguments', ast) + local len = #ast + assertCompile(len > 2, 'expected at least two arguments', ast) local lhs = compile1(ast[2], scope, parent, {nval = 1})[1] local lastval = compile1(ast[3], scope, parent, {nval = 1})[1] -- avoid double-eval by introducing locals for possible side-effects - if #ast > 3 then lastval = once(lastval, ast[3], scope, parent) end - local out = ('(%s) %s (%s)'): + if len > 3 then lastval = once(lastval, ast[3], scope, parent) end + local out = ('(%s %s %s)'): format(tostring(lhs), op, tostring(lastval)) - for i = 4, #ast do -- variadic comparison - local nextval = once(compile1(ast[i], scope, parent, {nval = 1})[1], - ast[i], scope, parent) - out = (out .. " and ((%s) %s (%s))"): - format(tostring(lastval), op, tostring(nextval)) - lastval = nextval + if len > 3 then + for i = 4, len do -- variadic comparison + local nextval = once(compile1(ast[i], scope, parent, {nval = 1})[1], + ast[i], scope, parent) + out = (out .. " %s (%s %s %s)"): + format(chainOp or 'and', tostring(lastval), op, tostring(nextval)) + lastval = nextval + end + out = '(' .. out .. ')' end return out end @@ -1424,7 +1614,8 @@ defineComparatorSpecial('<') defineComparatorSpecial('>=') defineComparatorSpecial('<=') defineComparatorSpecial('=', '==') -defineComparatorSpecial('~=') +defineComparatorSpecial('~=', '~=', 'or') +defineComparatorSpecial('not=', '~=', 'or') local function defineUnarySpecial(op, realop) SPECIALS[op] = function(ast, scope, parent) @@ -1437,27 +1628,126 @@ end defineUnarySpecial('not', 'not ') defineUnarySpecial('#') +-- Save current macro scope +local macroCurrentScope = GLOBAL_SCOPE + -- Covert a macro function to a special form local function macroToSpecial(mac) return function(ast, scope, parent, opts) + local oldScope = macroCurrentScope + macroCurrentScope = scope local ok, transformed = pcall(mac, unpack(ast, 2)) + macroCurrentScope = oldScope assertCompile(ok, transformed, ast) - return compile1(transformed, scope, parent, opts) + local result = compile1(transformed, scope, parent, opts) + return result end end local function compile(ast, options) options = options or {} + local oldGlobals = allowedGlobals + allowedGlobals = options.allowedGlobals if options.indent == nil then options.indent = ' ' end local chunk = {} local scope = options.scope or makeScope(GLOBAL_SCOPE) local exprs = compile1(ast, scope, chunk, {tail = true}) keepSideEffects(exprs, chunk, nil, ast) + allowedGlobals = oldGlobals return flatten(chunk, options) end +-- map a function across all pairs in a table +local function quoteTmap(f, t) + local res = {} + for k,v in pairs(t) do + local nk, nv = f(k, v) + if nk then + res[nk] = nv + end + end + return res +end + +-- make a transformer for key / value table pairs, preserving all numeric keys +local function entryTransform(fk,fv) + return function(k, v) + if type(k) == 'number' then + return k,fv(v) + else + return fk(k),fv(v) + end + end +end + +-- consume everything return nothing +local function no() end + +local function mixedConcat(t, joiner) + local ret = "" + local s = "" + local seen = {} + for k,v in ipairs(t) do + table.insert(seen, k) + ret = ret .. s .. v + s = joiner + end + for k,v in pairs(t) do + if not(seen[k]) then + ret = ret .. s .. '[' .. k .. ']' .. '=' .. v + s = joiner + end + end + return ret +end + +-- expand a quoted form into a data literal, evaluating unquote +local function doQuote (form, scope, parent, runtime) + local q = function (x) return doQuote(x, scope, parent, runtime) end + -- vararg + if isVarg(form) then + assertCompile(not runtime, "quoted ... may only be used at compile time", form) + return "_VARARG" + -- symbol + elseif isSym(form) then + assertCompile(not runtime, "symbols may only be used at compile time", form) + return ("sym('%s')"):format(deref(form)) + -- unquote + elseif isList(form) and isSym(form[1]) and (deref(form[1]) == 'unquote') then + local payload = form[2] + local res = unpack(compile1(payload, scope, parent)) + return res[1] + -- list + elseif isList(form) then + assertCompile(not runtime, "lists may only be used at compile time", form) + local mapped = quoteTmap(entryTransform(no, q), form) + return 'list(' .. mixedConcat(mapped, ", ") .. ')' + -- table + elseif type(form) == 'table' then + local mapped = quoteTmap(entryTransform(q, q), form) + return '{' .. mixedConcat(mapped, ", ") .. '}' + -- string + elseif type(form) == 'string' then + return serializeString(form) + else + return tostring(form) + end +end + +SPECIALS['quote'] = function(ast, scope, parent) + assertCompile(#ast == 2, "quote only takes a single form") + local runtime, thisScope = true, scope + while thisScope do + thisScope = thisScope.parent + if thisScope == COMPILER_SCOPE then runtime = false end + end + return doQuote(ast[2], scope, parent, runtime) +end + local function compileStream(strm, options) options = options or {} + local oldGlobals = allowedGlobals + allowedGlobals = options.allowedGlobals if options.indent == nil then options.indent = ' ' end local scope = options.scope or makeScope(GLOBAL_SCOPE) local vals = {} @@ -1472,6 +1762,7 @@ local function compileStream(strm, options) }) keepSideEffects(exprs, chunk, nil, vals[i]) end + allowedGlobals = oldGlobals return flatten(chunk, options) end @@ -1484,6 +1775,40 @@ end --- Evaluation --- +-- Convert a fennel environment table to a Lua environment table. +-- This means automatically unmangling globals when getting a value, +-- and mangling values when setting a value. This means the original +-- env will see its values updated as expected, regardless of mangling rules. +local function wrapEnv(env) + return setmetatable({}, { + __index = function(_, key) + if type(key) == 'string' then + key = globalUnmangling(key) + end + return env[key] + end, + __newindex = function(_, key, value) + if type(key) == 'string' then + key = globalMangling(key) + end + env[key] = value + end, + -- checking the __pairs metamethod won't work automatically in Lua 5.1 + -- sadly, but it's important for 5.2+ and can be done manually in 5.1 + __pairs = function() + local pt = {} + for key, value in pairs(env) do + if type(key) == 'string' then + pt[globalUnmangling(key)] = value + else + pt[key] = value + end + end + return next, pt, nil + end, + }) +end + -- A custom traceback function for Fennel that looks similar to -- the Lua's debug.traceback. -- Use with xpcall to produce fennel specific stacktraces. @@ -1510,8 +1835,8 @@ local function traceback(msg, start) -- And some global info info.short_src = remap.short_src local mapping = remap[info.currentline] - -- Overwrite info with values from the mapping (mapping is now just integer, - -- but may eventually be a table + -- Overwrite info with values from the mapping (mapping is now + -- just integer, but may eventually be a table) info.currentline = mapping end if info.what == 'Lua' then @@ -1529,18 +1854,40 @@ local function traceback(msg, start) return table.concat(lines, '\n') end +local function currentGlobalNames(env) + local names = {} + for k in pairs(env or _G) do + k = globalUnmangling(k) + table.insert(names, k) + end + return names +end + local function eval(str, options, ...) options = options or {} + -- eval and dofile are considered "live" entry points, so we can assume + -- that the globals available at compile time are a reasonable allowed list + -- UNLESS there's a metatable on env, in which case we can't assume that + -- pairs will return all the effective globals; for instance openresty + -- sets up _G in such a way that all the globals are available thru + -- the __index meta method, but as far as pairs is concerned it's empty. + if options.allowedGlobals == nil and not getmetatable(options.env) then + options.allowedGlobals = currentGlobalNames(options.env) + end + local env = options.env and wrapEnv(options.env) local luaSource = compileString(str, options) - local loader = loadCode(luaSource, options.env, + local loader = loadCode(luaSource, env, options.filename and ('@' .. options.filename) or str) return loader(...) end -local function dofile_fennel(filename, options, ...) +local function dofileFennel(filename, options, ...) options = options or {sourcemap = true} + if options.allowedGlobals == nil then + options.allowedGlobals = currentGlobalNames(options.env) + end local f = assert(io.open(filename, "rb")) - local source = f:read("*all") + local source = f:read("*all"):gsub("^#![^\n]*\n", "") f:close() options.filename = options.filename or filename return eval(source, options, ...) @@ -1550,13 +1897,18 @@ end local function repl(options) local opts = options or {} + -- This would get set for us when calling eval, but we want to seed it + -- with a value that is persistent so it doesn't get reset on each eval. + if opts.allowedGlobals == nil then + options.allowedGlobals = currentGlobalNames(opts.env) + end - local env = opts.env or setmetatable({}, { + local env = opts.env and wrapEnv(opts.env) or setmetatable({}, { __index = _ENV or _G }) - local function defaultReadChunk() - io.write('>> ') + local function defaultReadChunk(parserState) + io.write(parserState.stackSize > 0 and '.. ' or '>> ') io.flush() local input = io.read() return input and input .. '\n' @@ -1591,12 +1943,68 @@ local function repl(options) -- Make parser local bytestream, clearstream = granulate(readChunk) local chars = {} - local read = parser(function() - local c = bytestream() + local read, reset = parser(function (parserState) + local c = bytestream(parserState) chars[#chars + 1] = c return c end) + local envdbg = (opts.env or _G)["debug"] + -- if the environment doesn't support debug.getlocal you can't save locals + local saveLocals = opts.saveLocals ~= false and envdbg and envdbg.getlocal + local saveSource = table. + concat({"local ___i___ = 1", + "while true do", + " local name, value = debug.getlocal(1, ___i___)", + " if(name and name ~= \"___i___\") then", + " ___replLocals___[name] = value", + " ___i___ = ___i___ + 1", + " else break end end"}, "\n") + + local spliceSaveLocals = function(luaSource) + -- we do some source munging in order to save off locals from each chunk + -- and reintroduce them to the beginning of the next chunk, allowing + -- locals to work in the repl the way you'd expect them to. + env.___replLocals___ = env.___replLocals___ or {} + local splicedSource = {} + for line in luaSource:gmatch("([^\n]+)\n?") do + table.insert(splicedSource, line) + end + -- reintroduce locals from the previous time around + local bind = "local %s = ___replLocals___['%s']" + for name in pairs(env.___replLocals___) do + table.insert(splicedSource, 1, bind:format(name, name)) + end + -- save off new locals at the end - if safe to do so (i.e. last line is a return) + if (string.match(splicedSource[#splicedSource], "^ *return .*$")) then + if (#splicedSource > 1) then + table.insert(splicedSource, #splicedSource, saveSource) + end + end + return table.concat(splicedSource, "\n") + end + + local scope = makeScope(GLOBAL_SCOPE) + + local replCompleter = function(text, from, to) + local matches = {} + local inputFragment = string.lower(text):sub(from, to):gsub("[%s)(]*(.+)", "%1") + + -- adds any matching keys from the provided generator/iterator to matches + local function addMatchesFromGen(next, param, state) + for k in next, param, state do + if #matches >= 40 then break -- cap completions at 40 to avoid overwhelming output + elseif inputFragment == k:sub(0, #inputFragment):lower() then table.insert(matches, k) end + end + end + addMatchesFromGen(pairs(env._ENV or env._G or {})) + addMatchesFromGen(pairs(env.___replLocals___ or {})) + addMatchesFromGen(pairs(SPECIALS or {})) + addMatchesFromGen(pairs(scope.specials or {})) + return matches + end + if options.registerCompleter then options.registerCompleter(replCompleter) end + -- REPL loop while true do chars = {} @@ -1605,16 +2013,21 @@ local function repl(options) if not ok then onError('Parse', parseok) clearstream() + reset() else if not parseok then break end -- eof local compileOk, luaSource = pcall(compile, x, { sourcemap = opts.sourcemap, - source = srcstring + source = srcstring, + scope = scope, }) if not compileOk then clearstream() onError('Compile', luaSource) -- luaSource is error message in this case else + if saveLocals then + luaSource = spliceSaveLocals(luaSource) + end local luacompileok, loader = pcall(loadCode, luaSource, env) if not luacompileok then clearstream() @@ -1636,6 +2049,8 @@ local function repl(options) end end +local macroLoaded = {} + local module = { parser = parser, granulate = granulate, @@ -1653,9 +2068,11 @@ local module = { gensym = gensym, eval = eval, repl = repl, - dofile = dofile_fennel, + dofile = dofileFennel, + macroLoaded = macroLoaded, path = "./?.fnl;./?/init.fnl", - traceback = traceback + traceback = traceback, + version = "0.2.1", } local function searchModule(modulename) @@ -1670,14 +2087,14 @@ local function searchModule(modulename) end end -module.make_searcher = function(options) +module.makeSearcher = function(options) return function(modulename) local opts = {} for k,v in pairs(options or {}) do opts[k] = v end local filename = searchModule(modulename) if filename then return function(modname) - return dofile_fennel(filename, opts, modname) + return dofileFennel(filename, opts, modname) end end end @@ -1685,7 +2102,8 @@ end -- This will allow regular `require` to work with Fennel: -- table.insert(package.loaders, fennel.searcher) -module.searcher = module.make_searcher() +module.searcher = module.makeSearcher() +module.make_searcher = module.makeSearcher -- oops backwards compatibility local function makeCompilerEnv(ast, scope, parent) return setmetatable({ @@ -1702,60 +2120,136 @@ local function makeCompilerEnv(ast, scope, parent) -- via fennel.myfun, for example (fennel.eval "(print 1)"). list = list, sym = sym, - [globalMangling("list?")] = isList, - [globalMangling("multi-sym?")] = isMultiSym, - [globalMangling("sym?")] = isSym, - [globalMangling("table?")] = isTable, - [globalMangling("varg?")] = isVarg, + unpack = unpack, + gensym = function() return sym(gensym(macroCurrentScope)) end, + ["list?"] = isList, + ["multi-sym?"] = isMultiSym, + ["sym?"] = isSym, + ["table?"] = isTable, + ["sequence?"] = isSequence, + ["varg?"] = isVarg, + ["get-scope"] = function() return macroCurrentScope end, + ["in-scope?"] = function(symbol) + return macroCurrentScope.manglings[tostring(symbol)] + end }, { __index = _ENV or _G }) end -SPECIALS['require-macros'] = function(ast, scope, parent) - for i = 2, #ast do - local filename = assertCompile(searchModule(ast[i]), - ast[i] .. " not found.", ast) - local mod = dofile_fennel(filename, {env=makeCompilerEnv(ast, scope, parent)}) - for k, v in pairs(assertCompile(isTable(mod), 'expected ' .. ast[i] .. - 'module to be table', ast)) do - scope.specials[k] = macroToSpecial(v) +local function macroGlobals(env, globals) + local allowed = {} + for k in pairs(env) do + local g = globalUnmangling(k) + table.insert(allowed, g) + end + if globals then + for _, k in pairs(globals) do + table.insert(allowed, k) end end + return allowed +end + +local function addMacros(macros, ast, scope) + assertCompile(isTable(macros), 'expected macros to be table', ast) + for k, v in pairs(macros) do + scope.specials[k] = macroToSpecial(v) + end +end + +local function loadMacros(modname, ast, scope, parent) + local filename = assertCompile(searchModule(modname), + modname .. " not found.", ast) + local env = makeCompilerEnv(ast, scope, parent) + local globals = macroGlobals(env, currentGlobalNames()) + return dofileFennel(filename, { env = env, allowedGlobals = globals, + scope = COMPILER_SCOPE }) +end + +SPECIALS['require-macros'] = function(ast, scope, parent) + assertCompile(#ast == 2, "Expected one module name argument", ast) + local modname = ast[2] + if not macroLoaded[modname] then + macroLoaded[modname] = loadMacros(modname, ast, scope, parent) + end + addMacros(macroLoaded[modname], ast, scope, parent) +end + +local function evalCompiler(ast, scope, parent) + local luaSource = compile(ast, { scope = makeScope(COMPILER_SCOPE) }) + local loader = loadCode(luaSource, wrapEnv(makeCompilerEnv(ast, scope, parent))) + return loader() +end + +SPECIALS['macros'] = function(ast, scope, parent) + assertCompile(#ast == 2, "Expected one table argument", ast) + local macros = evalCompiler(ast[2], scope, parent) + addMacros(macros, ast, scope, parent) end SPECIALS['eval-compiler'] = function(ast, scope, parent) local oldFirst = ast[1] ast[1] = sym('do') - local luaSource = compile(ast, { scope = makeScope(COMPILER_SCOPE) }) + local val = evalCompiler(ast, scope, parent) ast[1] = oldFirst - local loader = loadCode(luaSource, makeCompilerEnv(ast, scope, parent)) - loader() + return val end -- Load standard macros local stdmacros = [===[ {"->" (fn [val ...] (var x val) - (each [_ elt (ipairs [...])] - (table.insert elt 2 x) - (set x elt)) + (each [_ e (ipairs [...])] + (let [elt (if (list? e) e (list e))] + (table.insert elt 2 x) + (set x elt))) x) "->>" (fn [val ...] (var x val) - (each [_ elt (pairs [...])] - (table.insert elt x) - (set x elt)) + (each [_ e (pairs [...])] + (let [elt (if (list? e) e (list e))] + (table.insert elt x) + (set x elt))) x) - :defn (fn [name args ...] - (assert (sym? name) "defn: function names must be symbols") - (list (sym :fn) name args ...)) + "-?>" (fn [val ...] + (if (= 0 (# [...])) + val + (let [els [...] + e (table.remove els 1) + el (if (list? e) e (list e)) + tmp (gensym)] + (table.insert el 2 tmp) + `(let [@tmp @val] + (if @tmp + (-?> @el @(unpack els)) + @tmp))))) + "-?>>" (fn [val ...] + (if (= 0 (# [...])) + val + (let [els [...] + e (table.remove els 1) + el (if (list? e) e (list e)) + tmp (gensym)] + (table.insert el tmp) + `(let [@tmp @val] + (if @tmp + (-?>> @el @(unpack els)) + @tmp))))) + :doto (fn [val ...] + (let [name (gensym) + form `(let [@name @val])] + (each [_ elt (pairs [...])] + (table.insert elt 2 name) + (table.insert form elt)) + (table.insert form name) + form)) :when (fn [condition body1 ...] (assert body1 "expected body") - (list (sym 'if') condition - (list (sym 'do') body1 ...))) + `(if @condition + (do @body1 @...))) :partial (fn [f ...] (let [body (list f ...)] (table.insert body _VARARG) - (list (sym "fn") [_VARARG] body))) + `(fn [@_VARARG] @body))) :lambda (fn [...] (let [args [...] has-internal-name? (sym? (. args 1)) @@ -1766,19 +2260,112 @@ local stdmacros = [===[ (if (and (not (: (tostring a) :match "^?")) (~= (tostring a) "...")) (table.insert args arity-check-position - (list (sym "assert") - (list (sym "~=") (sym "nil") a) - (: "Missing argument %s on %s:%s" - :format (tostring a) - (or a.filename "unknown") - (or a.line "?")))))) - (list (sym "fn") ((or unpack table.unpack) args)))) -} + `(assert (~= nil @a) + (: "Missing argument %s on %s:%s" + :format @(tostring a) + @(or a.filename "unknown") + @(or a.line "?")))))) + `(fn @(unpack args)))) + :match +(fn match [val ...] + ;; this function takes the AST of values and a single pattern and returns a + ;; condition to determine if it matches as well as a list of bindings to + ;; introduce for the duration of the body if it does match. + (fn match-pattern [vals pattern unifications] + ;; we have to assume we're matching against multiple values here until we + ;; know we're either in a multi-valued clause (in which case we know the # + ;; of vals) or we're not, in which case we only care about the first one. + (let [[val] vals] + (if (and (sym? pattern) ; unification with outer locals (or nil) + (or (in-scope? pattern) + (= :nil (tostring pattern)))) + (values `(= @val @pattern) []) + ;; unify a local we've seen already + (and (sym? pattern) + (. unifications (tostring pattern))) + (values `(= @(. unifications (tostring pattern)) @val) []) + ;; bind a fresh local + (sym? pattern) + (do (if (~= (tostring pattern) "_") + (tset unifications (tostring pattern) val)) + (values (if (: (tostring pattern) :find "^?") + true `(~= @(sym :nil) @val)) + [pattern val])) + ;; multi-valued patterns (represented as lists) + (list? pattern) + (let [condition `(and) + bindings []] + (each [i pat (ipairs pattern)] + (let [(subcondition subbindings) (match-pattern [(. vals i)] pat + unifications)] + (table.insert condition subcondition) + (each [_ b (ipairs subbindings)] + (table.insert bindings b)))) + (values condition bindings)) + ;; table patterns) + (= (type pattern) :table) + (let [condition `(and (= (type @val) :table)) + bindings []] + (each [k pat (pairs pattern)] + (if (and (sym? pat) (= "&" (tostring pat))) + (do (assert (not (. pattern (+ k 2))) + "expected rest argument in final position") + (table.insert bindings (. pattern (+ k 1))) + (table.insert bindings [`(select @k ((or unpack table.unpack) + @val))])) + (and (= :number (type k)) + (= "&" (tostring (. pattern (- k 1))))) + nil ; don't process the pattern right after &; already got it + (let [subval `(. @val @k) + (subcondition subbindings) (match-pattern [subval] pat + unifications)] + (table.insert condition subcondition) + (each [_ b (ipairs subbindings)] + (table.insert bindings b))))) + (values condition bindings)) + ;; literal value + (values `(= @val @pattern) [])))) + (fn match-condition [vals clauses] + (let [out `(if)] + (for [i 1 (# clauses) 2] + (let [pattern (. clauses i) + body (. clauses (+ i 1)) + (condition bindings) (match-pattern vals pattern {})] + (table.insert out condition) + (table.insert out `(let @bindings @body)))) + out)) + ;; how many multi-valued clauses are there? return a list of that many gensyms + (fn val-syms [clauses] + (let [syms (list (gensym))] + (for [i 1 (# clauses) 2] + (if (list? (. clauses i)) + (each [valnum (ipairs (. clauses i))] + (if (not (. syms valnum)) + (tset syms valnum (gensym)))))) + syms)) + ;; wrap it in a way that prevents double-evaluation of the matched value + (let [clauses [...] + vals (val-syms clauses)] + (if (~= 0 (% (# clauses) 2)) ; treat odd final clause as default + (table.insert clauses (# clauses) (sym :_))) + ;; protect against multiple evaluation of the value, bind against as + ;; many values as we ever match against in the clauses. + (list (sym :let) [vals val] + (match-condition vals clauses)))) + } ]===] -for name, fn in pairs(eval(stdmacros, { - env = makeCompilerEnv(nil, GLOBAL_SCOPE, {}) -})) do - SPECIALS[name] = macroToSpecial(fn) +do + local env = makeCompilerEnv(nil, COMPILER_SCOPE, {}) + for name, fn in pairs(eval(stdmacros, { + env = env, + scope = makeScope(COMPILER_SCOPE), + -- assume the code to load globals doesn't have any mistaken globals, + -- otherwise this can be problematic when loading fennel in contexts + -- where _G is an empty table with an __index metamethod. (openresty) + allowedGlobals = false, + })) do + SPECIALS[name] = macroToSpecial(fn) + end end SPECIALS['λ'] = SPECIALS['lambda'] diff --git a/lib/fennelview.lua b/lib/fennelview.lua index 1259f6e..4d1033b 100644 --- a/lib/fennelview.lua +++ b/lib/fennelview.lua @@ -1,132 +1,105 @@ -local function _0_(str) +local function view_quote(str) return ("\"" .. str:gsub("\"", "\\\"") .. "\"") end -local quote = _0_ -local short_control_char_escapes = ({["\11"] = "\\v", ["\12"] = "\\f", ["\13"] = "\\r", ["\7"] = "\\a", ["\8"] = "\\b", ["\9"] = "\\t", ["\n"] = "\\n"}) -local function _1_() - local long = ({}) +local short_control_char_escapes = {["\11"] = "\\v", ["\12"] = "\\f", ["\13"] = "\\r", ["\7"] = "\\a", ["\8"] = "\\b", ["\9"] = "\\t", ["\n"] = "\\n"} +local long_control_char_esapes +do + local long = {} for i = 0, 31 do local ch = string.char(i) - local function _2_() - if not short_control_char_escapes[ch] then - short_control_char_escapes[ch] = ("\\" .. i) - long[ch] = ("\\%03d"):format(i) - return nil - end + if not short_control_char_escapes[ch] then + short_control_char_escapes[ch] = ("\\" .. i) + long[ch] = ("\\%03d"):format(i) end - _2_() end - return long + long_control_char_esapes = long end -local long_control_char_esapes = _1_() -local function _2_(str) +local function escape(str) local str = str:gsub("\\", "\\\\") local str = str:gsub("(%c)%f[0-9]", long_control_char_esapes) return str:gsub("%c", short_control_char_escapes) end -local escape = _2_ -local function _3_(k, len) - return ((type(k)) == ("number") and (1) <= (k) and (k) <= (len) and (math.floor(k)) == (k)) +local function sequence_key_3f(k, len) + return ((type(k) == "number") and (1 <= k) and (k <= len) and (math.floor(k) == k)) end -local sequence_key_3f = _3_ -local type_order = ({["function"] = 5, boolean = 2, number = 1, string = 3, table = 4, thread = 7, userdata = 6}) -local function _4_(a, b) +local type_order = {["function"] = 5, boolean = 2, number = 1, string = 3, table = 4, thread = 7, userdata = 6} +local function sort_keys(a, b) local ta = type(a) local tb = type(b) - if ((ta) == (tb) and (ta) ~= ("boolean") and ((ta) == ("string") or (ta) == ("number"))) then - return (a) < (b) + if ((ta == tb) and (ta ~= "boolean") and ((ta == "string") or (ta == "number"))) then + return (a < b) else local dta = type_order[a] local dtb = type_order[b] if (dta and dtb) then - return (dta) < (dtb) + return (dta < dtb) elseif dta then return true elseif dtb then return false elseif "else" then - return (ta) < (tb) + return (ta < tb) end end end -local sort_keys = _4_ -local function _5_(t) +local function get_sequence_length(t) local len = 1 for i in ipairs(t) do len = i end return len end -local get_sequence_length = _5_ -local function _6_(t) - local keys = ({}) +local function get_nonsequential_keys(t) + local keys = {} local sequence_length = get_sequence_length(t) for k in pairs(t) do - local function _7_() - if not sequence_key_3f(k, sequence_length) then - return table.insert(keys, k) - end + if not sequence_key_3f(k, sequence_length) then + table.insert(keys, k) end - _7_() end table.sort(keys, sort_keys) return keys, sequence_length end -local get_nonsequential_keys = _6_ -local function recur(t, appearances) - local function _7_() - if (type(t)) == ("table") then - if not appearances[t] then - appearances[t] = 1 - for k, v in pairs(t) do - recur(k, appearances) - recur(v, appearances) - end - return nil - end - else - if (t and (t) == (t)) then - appearances[t] = ((appearances[t] or 0) + 1) - return nil +local function count_table_appearances(t, appearances) + if (type(t) == "table") then + if not appearances[t] then + appearances[t] = 1 + for k, v in pairs(t) do + count_table_appearances(k, appearances) + count_table_appearances(v, appearances) end end + else + if (t and (t == t)) then + appearances[t] = ((appearances[t] or 0) + 1) + end end - _7_() return appearances end -local count_table_appearances = recur local put_value = nil -local function _7_(self, ...) - for _, v in ipairs(({...})) do +local function puts(self, ...) + for _, v in ipairs({...}) do table.insert(self.buffer, v) end return nil end -local puts = _7_ -local function _8_(self) +local function tabify(self) return puts(self, "\n", self.indent:rep(self.level)) end -local tabify = _8_ -local function _9_(self, v) - return (self.ids[v]) ~= (nil) +local function already_visited_3f(self, v) + return (self.ids[v] ~= nil) end -local already_visited_3f = _9_ -local function _10_(self, v) +local function get_id(self, v) local id = self.ids[v] - local function _11_() - if not id then - local tv = type(v) - id = ((self["max-ids"][tv] or 0) + 1) - self["max-ids"][tv] = id - self.ids[v] = id - return nil - end + if not id then + local tv = type(v) + id = ((self["max-ids"][tv] or 0) + 1) + self["max-ids"][tv] = id + self.ids[v] = id end - _11_() return tostring(id) end -local get_id = _10_ -local function _11_(self, t, length) +local function put_sequential_table(self, t, length) puts(self, "[") self.level = (self.level + 1) for i = 1, length do @@ -136,16 +109,14 @@ local function _11_(self, t, length) self.level = (self.level - 1) return puts(self, " ]") end -local put_sequential_table = _11_ -local function _12_(self, k) - if ((type(k)) == ("string") and k:find("^[%w?\\^_`!#$%&*+-./@~:|<=>]+$")) then +local function put_key(self, k) + if ((type(k) == "string") and k:find("^[-%w?\\^_`!#$%&*+./@~:|<=>]+$")) then return puts(self, ":", k) else return put_value(self, k) end end -local put_key = _12_ -local function _13_(self, t) +local function put_kv_table(self, t) puts(self, "{") self.level = (self.level + 1) for k, v in pairs(t) do @@ -158,44 +129,42 @@ local function _13_(self, t) tabify(self) return puts(self, "}") end -local put_kv_table = _13_ -local function _14_(self, t) +local function put_table(self, t) if already_visited_3f(self, t) then return puts(self, "#") - elseif (self.level) >= (self.depth) then + elseif (self.level >= self.depth) then return puts(self, "{...}") elseif "else" then local non_seq_keys, length = get_nonsequential_keys(t) local id = get_id(self, t) - if (self.appearances[t]) > (1) then + if (self.appearances[t] > 1) then return puts(self, "#<", id, ">") - elseif ((#non_seq_keys) == (0) and (#t) == (0)) then + elseif ((#non_seq_keys == 0) and (#t == 0)) then return puts(self, "{}") - elseif (#non_seq_keys) == (0) then + elseif (#non_seq_keys == 0) then return put_sequential_table(self, t, length) elseif "else" then return put_kv_table(self, t) end end end -local put_table = _14_ -local function _15_(self, v) +local function _0_(self, v) local tv = type(v) - if (tv) == ("string") then - return puts(self, quote(escape(v))) - elseif ((tv) == ("number") or (tv) == ("boolean") or (tv) == ("nil")) then + if (tv == "string") then + return puts(self, view_quote(escape(v))) + elseif ((tv == "number") or (tv == "boolean") or (tv == "nil")) then return puts(self, tostring(v)) - elseif (tv) == ("table") then + elseif (tv == "table") then return put_table(self, v) elseif "else" then - return puts(self, "#<", tv, " ", get_id(self, v), ">") + return puts(self, "#<", tostring(v), ">") end end -put_value = _15_ -local function _16_(root, options) - local options = (options or ({})) - local inspector = ({["max-ids"] = ({}), appearances = count_table_appearances(root, ({})), buffer = ({}), depth = (options.depth or 128), ids = ({}), indent = (options.indent or " "), level = 0}) +put_value = _0_ +local function fennelview(root, options) + local options = (options or {}) + local inspector = {["max-ids"] = {}, appearances = count_table_appearances(root, {}), buffer = {}, depth = (options.depth or 128), ids = {}, indent = (options.indent or " "), level = 0} put_value(inspector, root) return table.concat(inspector.buffer) end -return _16_ +return fennelview diff --git a/lib/intersect.lua b/lib/intersect.lua deleted file mode 100644 index 10ad856..0000000 --- a/lib/intersect.lua +++ /dev/null @@ -1,15 +0,0 @@ --- https://love2d.org/wiki/General_math -return function(l1p1x,l1p1y, l1p2x,l1p2y, l2p1x,l2p1y, l2p2x,l2p2y) - local a1,b1,a2,b2 = l1p2y-l1p1y, l1p1x-l1p2x, l2p2y-l2p1y, l2p1x-l2p2x - local c1,c2 = a1*l1p1x+b1*l1p1y, a2*l2p1x+b2*l2p1y - local det,x,y = a1*b2 - a2*b1 - if det==0 then return false, "The lines are parallel." end - x,y = (b2*c1-b1*c2)/det, (a1*c2-a2*c1)/det - if not (math.min(l1p1x,l1p2x) <= x and x <= math.max(l1p1x,l1p2x) and - math.min(l1p1y,l1p2y) <= y and y <= math.max(l1p1y,l1p2y)) or - not (math.min(l2p1x,l2p2x) <= x and x <= math.max(l2p1x,l2p2x) and - math.min(l2p1y,l2p2y) <= y and y <= math.max(l2p1y,l2p2y)) then - return false, "The lines don't intersect." - end - return x,y -end diff --git a/lib/lume.lua b/lib/lume.lua index a5f0d9a..83650e0 100644 --- a/lib/lume.lua +++ b/lib/lume.lua @@ -1,20 +1,34 @@ -- -- lume -- --- Copyright (c) 2015 rxi +-- Copyright (c) 2018 rxi -- --- This library is free software; you can redistribute it and/or modify it --- under the terms of the MIT license. See LICENSE for details. +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. -- -local lume = { _version = "2.2.3" } +local lume = { _version = "2.3.0" } local pairs, ipairs = pairs, ipairs local type, assert, unpack = type, assert, unpack or table.unpack local tostring, tonumber = tostring, tonumber local math_floor = math.floor local math_ceil = math.ceil -local math_random = math.random local math_atan2 = math.atan2 or math.atan local math_sqrt = math.sqrt local math_abs = math.abs @@ -111,15 +125,20 @@ function lume.angle(x1, y1, x2, y2) end +function lume.vector(angle, magnitude) + return math.cos(angle) * magnitude, math.sin(angle) * magnitude +end + + function lume.random(a, b) if not a then a, b = 0, 1 end if not b then b = 0 end - return a + math_random() * (b - a) + return a + math.random() * (b - a) end function lume.randomchoice(t) - return t[math_random(#t)] + return t[math.random(#t)] end @@ -139,7 +158,7 @@ end function lume.isarray(x) - return (type(x) == "table" and x[1] ~= nil) and true or false + return type(x) == "table" and x[1] ~= nil end @@ -194,7 +213,7 @@ end function lume.shuffle(t) local rtn = {} for i = 1, #t do - local r = math_random(i) + local r = math.random(i) if r ~= i then rtn[i] = rtn[r] end @@ -283,7 +302,7 @@ function lume.reduce(t, fn, first) end -function lume.set(t) +function lume.unique(t) local rtn = {} for k in pairs(lume.invert(t)) do rtn[#rtn + 1] = k @@ -658,7 +677,7 @@ end function lume.uuid() local fn = function(x) - local r = math_random(16) - 1 + local r = math.random(16) - 1 r = (x == "x") and (r + 1) or (r % 4) + 9 return ("0123456789abcdef"):sub(r, r) end @@ -705,7 +724,9 @@ end local ripairs_iter = function(t, i) i = i - 1 local v = t[i] - if v then return i, v end + if v ~= nil then + return i, v + end end function lume.ripairs(t) @@ -735,15 +756,6 @@ function lume.color(str, mul) end -function lume.rgba(color) - local a = math_floor((color / 16777216) % 256) - local r = math_floor((color / 65536) % 256) - local g = math_floor((color / 256) % 256) - local b = math_floor((color) % 256) - return r, g, b, a -end - - local chain_mt = {} chain_mt.__index = lume.map(lume.filter(lume, iscallable, true), function(fn) diff --git a/lib/sfxr.lua b/lib/sfxr.lua deleted file mode 100644 index d44b06e..0000000 --- a/lib/sfxr.lua +++ /dev/null @@ -1,1536 +0,0 @@ --- sfxr.lua --- original by Tomas Pettersson, ported to Lua by nucular - ---[[ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -]]-- - ---[[-- -A port of the sfxr sound effect synthesizer to pure Lua, designed to be used -together with the *awesome* [LÖVE](https://love2d.org) game framework. -]]-- --- @module sfxr -local sfxr = {} -local bit = bit32 or require("bit") - --- Constants - ---- The module version (SemVer format) --- @within Constants -sfxr.VERSION = "0.0.2" - ---- [Waveform](https://en.wikipedia.org/wiki/Waveform) constants --- @within Constants --- @field SQUARE [square wave](https://en.wikipedia.org/wiki/Square_wave) (`= 0`) --- @field SAW [saw wave](https://en.wikipedia.org/wiki/Sawtooth_wave) (`= 1`) --- @field SINE [sine wave](https://en.wikipedia.org/wiki/Sine_wave) (`= 2`) --- @field NOISE [white noise](https://en.wikipedia.org/wiki/White_noise) (`= 3`) -sfxr.WAVEFORM = { - SQUARE = 0, - [0] = 0, - SAW = 1, - [1] = 1, - SINE = 2, - [2] = 2, - NOISE = 3, - [3] = 3 -} - ---- [Sampling rate](https://en.wikipedia.org/wiki/Sampling_(signal_processing)#Sampling_rate) constants --- (use the number values directly, these are just for lookup) --- @within Constants --- @field 22050 22.05 kHz (`= 22050`) --- @field 44100 44.1 kHz (`= 44100`) -sfxr.SAMPLERATE = { - [22050] = 22050, --- 22.05 kHz - [44100] = 44100 --- 44.1 kHz -} - ---- [Bit depth](https://en.wikipedia.org/wiki/Audio_bit_depth) constants --- (use the number values directly, these are just for lookup) --- @within Constants --- @field 0 floating point bit depth, -1 to 1 (`= 0`) --- @field 8 unsigned 8 bit, 0x00 to 0xFF (`= 8`) --- @field 16 unsigned 16 bit, 0x0000 to 0xFFFF (`= 16`) -sfxr.BITDEPTH = { - [0] = 0, - [16] = 16, - [8] = 8 -} - ---- [Endianness](https://en.wikipedia.org/wiki/Endianness) constants --- @within Constants --- @field LITTLE little endian (`= 0`) --- @field BIG big endian (`= 1`) -sfxr.ENDIANNESS = { - LITTLE = 0, - [0] = 0, - BIG = 1, - [1] = 1 -} - --- Utilities - ---- Truncate a number to an unsigned integer. --- @tparam number n a (signed) number --- @treturn int the number, truncated and unsigned -local function trunc(n) - if n >= 0 then - return math.floor(n) - else - return -math.floor(-n) - end -end - ---- Set the random seed and initializes the generator. --- @tparam number seed the random seed -local function setseed(seed) - math.randomseed(seed) - for i=0, 5 do - math.random() - end -end - ---- Return a random number between low and high. --- @tparam number low the lower bound --- @tparam number high the upper bound --- @treturn number a random number where `low < n < high` -local function random(low, high) - return low + math.random() * (high - low) -end - ---- Return a random boolean weighted towards false by n. --- w = 1: uniform distribution --- w = n: false is n times as likely as true --- Note: n < 0 do not work, use `not maybe(w)` instead --- @tparam[opt=1] number w the weight towards false --- @treturn bool a random boolean -local function maybe(w) - return trunc(random(0, w or 1)) == 0 -end - ---- Clamp n between min and max. --- @tparam number n the number --- @tparam number min the lower bound --- @tparam number max the upper bound --- @treturn number the number where `min <= n <= max` -local function clamp(n, min, max) - return math.max(min or -math.huge, math.min(max or math.huge, n)) -end - ---- Copy a table (shallow) or a primitive. --- @param t a table or primitive --- @return a copy of t -local function shallowcopy(t) - if type(t) == "table" then - local t2 = {} - for k,v in pairs(t) do - t2[k] = v - end - return t2 - else - return t - end -end - ---- Recursively merge table t2 into t1. --- @tparam tab t1 a table --- @tparam tab t2 a table to merge into t1 --- @treturn tab t1 -local function mergetables(t1, t2) - for k, v in pairs(t2) do - if type(v) == "table" then - if type(t1[k] or false) == "table" then - mergetables(t1[k] or {}, t2[k] or {}) - else - t1[k] = v - end - else - t1[k] = v - end - end - return t1 -end - ---- Pack a number into a IEEE754 32-bit big-endian floating point binary string. --- [source](https://stackoverflow.com/questions/14416734/) --- @tparam number number a number --- @treturn string a binary string -local function packIEEE754(number) - if number == 0 then - return string.char(0x00, 0x00, 0x00, 0x00) - elseif number ~= number then - return string.char(0xFF, 0xFF, 0xFF, 0xFF) - else - local sign = 0x00 - if number < 0 then - sign = 0x80 - number = -number - end - local mantissa, exponent = math.frexp(number) - exponent = exponent + 0x7F - if exponent <= 0 then - mantissa = math.ldexp(mantissa, exponent - 1) - exponent = 0 - elseif exponent > 0 then - if exponent >= 0xFF then - return string.char(sign + 0x7F, 0x80, 0x00, 0x00) - elseif exponent == 1 then - exponent = 0 - else - mantissa = mantissa * 2 - 1 - exponent = exponent - 1 - end - end - mantissa = math.floor(math.ldexp(mantissa, 23) + 0.5) - return string.char( - sign + math.floor(exponent / 2), - (exponent % 2) * 0x80 + math.floor(mantissa / 0x10000), - math.floor(mantissa / 0x100) % 0x100, - mantissa % 0x100) - end -end - ---- Unpack a IEEE754 32-bit big-endian floating point string to a number. --- [source](https://stackoverflow.com/questions/14416734/) --- @tparam string packed a binary string --- @treturn number a number -local function unpackIEEE754(packed) - local b1, b2, b3, b4 = string.byte(packed, 1, 4) - local exponent = (b1 % 0x80) * 0x02 + math.floor(b2 / 0x80) - local mantissa = math.ldexp(((b2 % 0x80) * 0x100 + b3) * 0x100 + b4, -23) - if exponent == 0xFF then - if mantissa > 0 then - return 0 / 0 - else - mantissa = math.huge - exponent = 0x7F - end - elseif exponent > 0 then - mantissa = mantissa + 1 - else - exponent = exponent + 1 - end - if b1 >= 0x80 then - mantissa = -mantissa - end - return math.ldexp(mantissa, exponent - 0x7F) -end - ---- Construct and return a new @{Sound} instance. --- @treturn Sound a Sound instance -function sfxr.newSound(...) - local instance = setmetatable({}, sfxr.Sound) - instance:__init(...) - return instance -end - ---- The main Sound class. --- @type Sound -sfxr.Sound = {} -sfxr.Sound.__index = sfxr.Sound - ---- Initialize the Sound instance. --- Called by @{sfxr.newSound|the constructor}. -function sfxr.Sound:__init() - --- Number of supersampling passes to perform (*default* 8) - -- @within Parameters - self.supersampling = 8 - --- Repeat speed: - -- Times to repeat the frequency slide over the course of the envelope - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Parameters - self.repeatspeed = 0.0 - --- The base @{WAVEFORM|waveform} (*default* @{WAVEFORM|SQUARE}) - -- @within Parameters - self.waveform = sfxr.WAVEFORM.SQUARE - - -- Build tables to store the parameters in - - --- **The sound volume and gain all samples are multiplied with** - -- @within Volume - self.volume = {} - --- **The [ASD envelope](https://en.wikipedia.org/wiki/Synthesizer#Attack_ - --Decay_Sustain_Release_.28ADSR.29_envelope) that controls the sound - -- amplitude (volume) over time** - -- @within Envelope - self.envelope = {} - --- **The base and minimum frequencies of the tone generator and their - -- slides** - -- @within Frequency - self.frequency = {} - --- **A [vibrato](https://en.wikipedia.org/wiki/Vibrato)-like amplitude - -- modulation effect** - -- SerializationVibrato - self.vibrato = {} - --- **Changes the frequency mid-sound to create a characteristic - -- "coin"-effect** - -- @within Change - self.change = {} - --- **The [duty](https://en.wikipedia.org/wiki/Duty_cycle) of the square - -- waveform** - -- @within Duty - self.duty = {} - --- **A simple [phaser](https://en.wikipedia.org/wiki/Phaser_(effect)) - -- effect** - -- @within Phaser - self.phaser = {} - --- **A [lowpass filter](https://en.wikipedia.org/wiki/Low-pass_filter) - -- effect** - -- @within Lowpass - self.lowpass = {} - --- **A [highpass filter](https://en.wikipedia.org/wiki/High-pass_filter) - -- effect** - -- @within Highpass - self.highpass = {} - - -- These are not affected by resetParameters() - - --- Master volume (*default* 0.5) - -- @within Volume - self.volume.master = 0.5 - --- Additional gain (*default* 0.5) - -- @within Volume - self.volume.sound = 0.5 - - self:resetParameters() -end - ---- Set all parameters to their default values. Does not affect --- @{self.supersampling|supersampling} and @{self.volume|volume}. --- Called by @{sfxr.Sound:__init|the initializer}. -function sfxr.Sound:resetParameters() - self.repeatspeed = 0.0 - self.waveform = sfxr.WAVEFORM.SQUARE - - --- Attack time: - -- Time the sound takes to reach its peak amplitude - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Envelope - self.envelope.attack = 0.0 - --- Sustain time: - -- Time the sound stays on its peak amplitude - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Envelope - self.envelope.sustain = 0.3 - --- Sustain punch: - -- Amount by which the sound peak amplitude is increased at the start of the - -- sustain time - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Envelope - self.envelope.punch = 0.0 - --- Decay time: - -- Time the sound takes to decay after its sustain time - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Envelope - self.envelope.decay = 0.4 - - --- Start frequency: - -- Base tone of the sound, before sliding - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Frequency - self.frequency.start = 0.3 - --- Min frequency: - -- Tone below which the sound will get cut off - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Frequency - self.frequency.min = 0.0 - --- Slide: - -- Amount by which the frequency is increased or decreased over time - -- (*default* 0.0, *min* -1.0, *max* 1.0) - -- @within Frequency - self.frequency.slide = 0.0 - --- Delta slide: - -- Amount by which the slide is increased or decreased over time - -- (*default* 0.0, *min* -1.0, *max* 1.0) - -- @within Frequency - self.frequency.dslide = 0.0 - - --- Vibrato depth: - -- Amount of amplitude modulation - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Vibrato - self.vibrato.depth = 0.0 - --- Vibrato speed: - -- Oscillation speed of the vibrato - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Vibrato - self.vibrato.speed = 0.0 - --- Vibrato delay: - -- Unused and unimplemented - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Vibrato - self.vibrato.delay = 0.0 - - --- Change amount: - -- Amount by which the frequency is changed mid-sound - -- (*default* 0.0, *min* -1.0, *max* 1.0) - -- @within Change - self.change.amount = 0.0 - --- Change speed: - -- Time before the frequency change happens - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Change - self.change.speed = 0.0 - - --- Square duty: - -- Width of the square wave pulse cycle (doesn't affect other waveforms) - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Duty - self.duty.ratio = 0.0 - --- Duty sweep: - -- Amount by which the square duty is increased or decreased over time - -- (*default* 0.0, *min* -1.0, *max* 1.0) - -- @within Duty - self.duty.sweep = 0.0 - - --- Phaser offset: - -- Amount by which the phaser signal is offset from the sound - -- (*default* 0.0, *min* -1.0, *max* 1.0) - -- @within Phaser - self.phaser.offset = 0.0 - --- Phaser sweep: - -- Amount by which the phaser offset is increased or decreased over time - -- (*default* 0.0, *min* -1.0, *max* 1.0) - -- @within Phaser - self.phaser.sweep = 0.0 - - --- Lowpass filter cutoff: - -- Lower bound for frequencies allowed to pass through this filter - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Lowpass - self.lowpass.cutoff = 1.0 - --- Lowpass filter cutoff sweep: - -- Amount by which the LP filter cutoff is increased or decreased - -- over time - -- (*default* 0.0, *min* -1.0, *max* 1.0) - -- @within Lowpass - self.lowpass.sweep = 0.0 - --- Lowpass filter resonance: - -- Amount by which certain resonant frequencies near the cutoff are - -- increased - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Lowpass - self.lowpass.resonance = 0.0 - --- Highpass filter cutoff: - -- Upper bound for frequencies allowed to pass through this filter - -- (*default* 0.0, *min* 0.0, *max* 1.0) - -- @within Highpass - self.highpass.cutoff = 0.0 - --- Highpass filter cutoff sweep: - -- Amount by which the HP filter cutoff is increased or decreased - -- over time - -- (*default* 0.0, *min* -1.0, *max* 1.0) - -- @within Highpass - self.highpass.sweep = 0.0 -end - ---- Clamp all parameters within their sane ranges. -function sfxr.Sound:sanitizeParameters() - self.repeatspeed = clamp(self.repeatspeed, 0, 1) - self.waveform = clamp(self.waveform, 0, #sfxr.WAVEFORM) - - self.envelope.attack = clamp(self.envelope.attack, 0, 1) - self.envelope.sustain = clamp(self.envelope.sustain, 0, 1) - self.envelope.punch = clamp(self.envelope.punch, 0, 1) - self.envelope.decay = clamp(self.envelope.decay, 0, 1) - - self.frequency.start = clamp(self.frequency.start, 0, 1) - self.frequency.min = clamp(self.frequency.min, 0, 1) - self.frequency.slide = clamp(self.frequency.slide, -1, 1) - self.frequency.dslide = clamp(self.frequency.dslide, -1, 1) - - self.vibrato.depth = clamp(self.vibrato.depth, 0, 1) - self.vibrato.speed = clamp(self.vibrato.speed, 0, 1) - self.vibrato.delay = clamp(self.vibrato.delay, 0, 1) - - self.change.amount = clamp(self.change.amount, -1, 1) - self.change.speed = clamp(self.change.speed, 0, 1) - - self.duty.ratio = clamp(self.duty.ratio, 0, 1) - self.duty.sweep = clamp(self.duty.sweep, -1, 1) - - self.phaser.offset = clamp(self.phaser.offset, -1, 1) - self.phaser.sweep = clamp(self.phaser.sweep, -1, 1) - - self.lowpass.cutoff = clamp(self.lowpass.cutoff, 0, 1) - self.lowpass.sweep = clamp(self.lowpass.sweep, -1, 1) - self.lowpass.resonance = clamp(self.lowpass.resonance, 0, 1) - self.highpass.cutoff = clamp(self.highpass.cutoff, 0, 1) - self.highpass.sweep = clamp(self.highpass.sweep, -1, 1) -end - ---- Generate the sound and yield the sample data. --- @tparam[opt=44100] SAMPLERATE rate the sampling rate --- @tparam[opt=0] BITDEPTH depth the bit depth --- @treturn function() a generator that yields the next sample when called --- @usage for s in sound:generate(44100, 0) do --- -- do something with s --- end --- @raise "invalid sampling rate: x", "invalid bit depth: x" -function sfxr.Sound:generate(rate, depth) - rate = rate or 44100 - depth = depth or 0 - assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate)) - assert(sfxr.BITDEPTH[depth], "invalid bit depth: " .. tostring(depth)) - - -- Initialize all locals - local fperiod, maxperiod - local slide, dslide - local square_duty, square_slide - local chg_mod, chg_time, chg_limit - - local phaserbuffer = {} - local noisebuffer = {} - - -- Initialize the sample buffers - for i=1, 1024 do - phaserbuffer[i] = 0 - end - - for i=1, 32 do - noisebuffer[i] = random(-1, 1) - end - - --- Reset the sound period - local function reset() - fperiod = 100 / (self.frequency.start^2 + 0.001) - maxperiod = 100 / (self.frequency.min^2 + 0.001) - period = trunc(fperiod) - - slide = 1.0 - self.frequency.slide^3 * 0.01 - dslide = -self.frequency.dslide^3 * 0.000001 - - square_duty = 0.5 - self.duty.ratio * 0.5 - square_slide = -self.duty.sweep * 0.00005 - - if self.change.amount >= 0 then - chg_mod = 1.0 - self.change.amount^2 * 0.9 - else - chg_mod = 1.0 + self.change.amount^2 * 10 - end - - chg_time = 0 - if self.change.speed == 1 then - chg_limit = 0 - else - chg_limit = trunc((1 - self.change.speed)^2 * 20000 + 32) - end - end - - local phase = 0 - reset() - - local second_sample = false - - local env_vol = 0 - local env_stage = 1 - local env_time = 0 - local env_length = {self.envelope.attack^2 * 100000, - self.envelope.sustain^2 * 100000, - self.envelope.decay^2 * 100000} - - local fphase = self.phaser.offset^2 * 1020 - if self.phaser.offset < 0 then fphase = -fphase end - local dphase = self.phaser.sweep^2 - if self.phaser.sweep < 0 then dphase = -dphase end - local ipp = 0 - - local iphase = math.abs(trunc(fphase)) - - local fltp = 0 - local fltdp = 0 - local fltw = self.lowpass.cutoff^3 * 0.1 - local fltw_d = 1 + self.lowpass.sweep * 0.0001 - local fltdmp = 5 / (1 + self.lowpass.resonance^2 * 20) * (0.01 + fltw) - fltdmp = clamp(fltdmp, nil, 0.8) - local fltphp = 0 - local flthp = self.highpass.cutoff^2 * 0.1 - local flthp_d = 1 + self.highpass.sweep * 0.0003 - - local vib_phase = 0 - local vib_speed = self.vibrato.speed^2 * 0.01 - local vib_amp = self.vibrato.depth * 0.5 - - local rep_time = 0 - local rep_limit = trunc((1 - self.repeatspeed)^2 * 20000 + 32) - if self.repeatspeed == 0 then - rep_limit = 0 - end - - -- The main closure (returned as a generator) - - local function next() - -- Repeat when needed - rep_time = rep_time + 1 - if rep_limit ~= 0 and rep_time >= rep_limit then - rep_time = 0 - reset() - end - - -- Update the change time and apply it if needed - chg_time = chg_time + 1 - if chg_limit ~= 0 and chg_time >= chg_limit then - chg_limit = 0 - fperiod = fperiod * chg_mod - end - - -- Apply the frequency slide and stuff - slide = slide + dslide - fperiod = fperiod * slide - - if fperiod > maxperiod then - fperiod = maxperiod - -- Fail if the minimum frequency is too small - if (self.frequency.min > 0) then - return nil - end - end - - -- Vibrato - local rfperiod = fperiod - if vib_amp > 0 then - vib_phase = vib_phase + vib_speed - -- Apply to the frequency period - rfperiod = fperiod * (1.0 + math.sin(vib_phase) * vib_amp) - end - - -- Update the period - period = trunc(rfperiod) - if (period < 8) then period = 8 end - - -- Update the square duty - square_duty = clamp(square_duty + square_slide, 0, 0.5) - - -- Volume envelopes - - env_time = env_time + 1 - - if env_time > env_length[env_stage] then - env_time = 0 - env_stage = env_stage + 1 - -- After the decay stop generating - if env_stage == 4 then - return nil - end - end - - -- Attack, Sustain, Decay/Release - if env_stage == 1 then - env_vol = env_time / env_length[1] - elseif env_stage == 2 then - env_vol = 1 + (1 - env_time / env_length[2])^1 * 2 * self.envelope.punch - elseif env_stage == 3 then - env_vol = 1 - env_time / env_length[3] - end - - -- Phaser - - fphase = fphase + dphase - iphase = clamp(math.abs(trunc(fphase)), nil, 1023) - - -- Filter stuff - - if flthp_d ~= 0 then - flthp = clamp(flthp * flthp_d, 0.00001, 0.1) - end - - -- And finally the actual tone generation and supersampling - - local ssample = 0 - for si = 0, self.supersampling-1 do - local sample = 0 - - phase = phase + 1 - - -- fill the noise buffer every period - if phase >= period then - --phase = 0 - phase = phase % period - if self.waveform == sfxr.WAVEFORM.NOISE then - for i = 1, 32 do - noisebuffer[i] = random(-1, 1) - end - end - end - - -- Tone generators ahead - - local fp = phase / period - - -- Square, including square duty - if self.waveform == sfxr.WAVEFORM.SQUARE then - if fp < square_duty then - sample = 0.5 - else - sample = -0.5 - end - - -- Sawtooth - elseif self.waveform == sfxr.WAVEFORM.SAWTOOTH then - sample = 1 - fp * 2 - - -- Sine - elseif self.waveform == sfxr.WAVEFORM.SINE then - sample = math.sin(fp * 2 * math.pi) - - -- Pitched white noise - elseif self.waveform == sfxr.WAVEFORM.NOISE then - sample = noisebuffer[trunc(phase * 32 / period) % 32 + 1] - end - - -- Apply the lowpass filter to the sample - - local pp = fltp - fltw = clamp(fltw * fltw_d, 0, 0.1) - if self.lowpass.cutoff ~= 1 then - fltdp = fltdp + (sample - fltp) * fltw - fltdp = fltdp - fltdp * fltdmp - else - fltp = sample - fltdp = 0 - end - fltp = fltp + fltdp - - -- Apply the highpass filter to the sample - - fltphp = fltphp + (fltp - pp) - fltphp = fltphp - (fltphp * flthp) - sample = fltphp - - -- Apply the phaser to the sample - - phaserbuffer[bit.band(ipp, 1023) + 1] = sample - sample = sample + phaserbuffer[bit.band(ipp - iphase + 1024, 1023) + 1] - ipp = bit.band(ipp + 1, 1023) - - -- Accumulation and envelope application - ssample = ssample + sample * env_vol - end - - -- Apply the volumes - ssample = (ssample / self.supersampling) * self.volume.master - ssample = ssample * (2 * self.volume.sound) - - -- Hard limit - ssample = clamp(ssample, -1, 1) - - -- Frequency conversion - second_sample = not second_sample - if rate == 22050 and second_sample then - -- hah! - local nsample = next() - if nsample then - return (ssample + nsample) / 2 - else - return nil - end - end - - -- bit conversions - if depth == 0 then - return ssample - elseif depth == 16 then - return trunc(ssample * 32000) - else - return trunc(ssample * 127 + 128) - end - end - - return next -end - ---- Get the maximum sample limit allowed by the current envelope. --- Does not take any other limits into account, so the returned count might be --- higher than samples actually generated. Still useful though. --- @tparam[opt=44100] SAMPLERATE rate the sampling rate --- @raise "invalid sampling rate: x", "invalid bit depth: x" -function sfxr.Sound:getEnvelopeLimit(rate) - rate = rate or 44100 - assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate)) - - local env_length = { - self.envelope.attack^2 * 100000, --- attack - self.envelope.sustain^2 * 100000, --- sustain - self.envelope.decay^2 * 100000 --- decay - } - local limit = trunc(env_length[1] + env_length[2] + env_length[3] + 2) - - return math.ceil(limit / (rate / 44100)) -end - ---- Generate the sound into a table. --- @tparam[opt=44100] SAMPLERATE rate the sampling rate --- @tparam[opt=0] BITDEPTH depth the bit depth --- @tparam[opt] {} tab the table to synthesize into --- @treturn {number,...} the table filled with sample data --- @treturn int the number of written samples (== #tab) --- @raise "invalid sampling rate: x", "invalid bit depth: x" -function sfxr.Sound:generateTable(rate, depth, tab) - rate = rate or 44100 - depth = depth or 0 - assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate)) - assert(sfxr.BITDEPTH[depth], "invalid bit depth: " .. tostring(depth)) - - -- this could really use table pre-allocation, but Lua doesn't provide that - local t = tab or {} - local i = 1 - for v in self:generate(rate, depth) do - t[i] = v - i = i + 1 - end - return t, i -end - ---- Generate the sound to a binary string. --- @tparam[opt=44100] SAMPLERATE rate the sampling rate --- @tparam[opt=16] BITDEPTH depth the bit depth (may not be @{BITDEPTH|0}) --- @tparam[opt=0] ENDIANNESS endianness the endianness (ignored when depth == 8) --- @treturn string a binary string of sample data --- @treturn int the number of written samples --- @raise "invalid sampling rate: x", "invalid bit depth: x", "invalid endianness: x" -function sfxr.Sound:generateString(rate, depth, endianness) - rate = rate or 44100 - depth = depth or 16 - endianness = endianness or 0 - assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate)) - assert(sfxr.BITDEPTH[depth] and depth ~= 0, "invalid bit depth: " .. tostring(depth)) - assert(sfxr.ENDIANNESS[endianness], "invalid endianness: " .. tostring(endianness)) - - local s = "" - --- buffer for arguments to string.char - local buf = {} - buf[100] = 0 - local bi = 1 - - local i = 0 - for v in self:generate(rate, depth) do - if depth == 8 then - buf[i] = v - bi = bi + 1 - else - if endianness == sfxr.ENDIANNESS.BIG then - buf[bi] = bit.rshift(v, 8) - buf[bi + 1] = bit.band(v, 0xFF) - bi = bi + 2 - else - buf[bi] = bit.band(v, 0xFF) - buf[bi + 1] = bit.rshift(v, 8) - bi = bi + 2 - end - end - - if bi >= 100 then - s = s .. string.char(unpack(buf)) - bi = 0 - end - i = i + 1 - end - - -- pass in up to 100 characters - s = s .. string.char(unpack(buf, i, 100)) - return s, i -end - ---- Synthesize the sound to a LÖVE SoundData instance. --- @tparam[opt=44100] SAMPLERATE rate the sampling rate --- @tparam[opt=0] BITDEPTH depth the bit depth --- @tparam[opt] love.sound.SoundData sounddata a SoundData instance (will be --- created if not passed) --- @treturn love.sound.SoundData a SoundData instance --- @treturn int the number of written samples --- @raise "invalid sampling rate: x", "invalid bit depth: x" -function sfxr.Sound:generateSoundData(rate, depth, sounddata) - rate = rate or 44100 - depth = depth or 0 - assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate)) - assert(sfxr.BITDEPTH[depth] and depth, "invalid bit depth: " .. tostring(depth)) - - local tab, count = self:generateTable(rate, depth) - - if count == 0 then - return nil - end - - local data = sounddata or love.sound.newSoundData(count, freq, bits, 1) - - for i = 0, #tab - 1 do - data:setSample(i, tab[i + 1]) - end - - return data, count -end - ---- Randomize all sound parameters --- @within Randomization --- @tparam[opt] number seed a random seed -function sfxr.Sound:randomize(seed) - if seed then setseed(seed) end - - local waveform = self.waveform - self:resetParameters() - self.waveform = waveform - - if maybe() then - self.repeatspeed = random(0, 1) - end - - if maybe() then - self.frequency.start = random(-1, 1)^3 + 0.5 - else - self.frequency.start = random(-1, 1)^2 - end - self.frequency.limit = 0 - self.frequency.slide = random(-1, 1)^5 - if self.frequency.start > 0.7 and self.frequency.slide > 0.2 then - self.frequency.slide = -self.frequency.slide - elseif self.frequency.start < 0.2 and self.frequency.slide <-0.05 then - self.frequency.slide = -self.frequency.slide - end - self.frequency.dslide = random(-1, 1)^3 - - self.duty.ratio = random(-1, 1) - self.duty.sweep = random(-1, 1)^3 - - self.vibrato.depth = random(-1, 1)^3 - self.vibrato.speed = random(-1, 1) - self.vibrato.delay = random(-1, 1) - - self.envelope.attack = random(-1, 1)^3 - self.envelope.sustain = random(-1, 1)^2 - self.envelope.punch = random(-1, 1)^2 - self.envelope.decay = random(-1, 1) - - if self.envelope.attack + self.envelope.sustain + self.envelope.decay < 0.2 then - self.envelope.sustain = self.envelope.sustain + 0.2 + random(0, 0.3) - self.envelope.decay = self.envelope.decay + 0.2 + random(0, 0.3) - end - - self.lowpass.resonance = random(-1, 1) - self.lowpass.cutoff = 1 - random(0, 1)^3 - self.lowpass.sweep = random(-1, 1)^3 - if self.lowpass.cutoff < 0.1 and self.lowpass.sweep < -0.05 then - self.lowpass.sweep = -self.lowpass.sweep - end - self.highpass.cutoff = random(0, 1)^3 - self.highpass.sweep = random(-1, 1)^5 - - self.phaser.offset = random(-1, 1)^3 - self.phaser.sweep = random(-1, 1)^3 - - self.change.speed = random(-1, 1) - self.change.amount = random(-1, 1) - - self:sanitizeParameters() -end - ---- Mutate all sound parameters --- @within Randomization --- @tparam[opt=1] number amount by how much to mutate the parameters --- @tparam[opt] number seed a random seed --- @tparam[changefreq=true] bool changefreq whether to change the frequency parameters -function sfxr.Sound:mutate(amount, seed, changefreq) - if seed then setseed(seed) end - local amount = (amount or 1) - local a = amount / 20 - local b = (1 - a) * 10 - local changefreq = (changefreq == nil) and true or changefreq - - if changefreq == true then - if maybe(b) then self.frequency.start = self.frequency.start + random(-a, a) end - if maybe(b) then self.frequency.slide = self.frequency.slide + random(-a, a) end - if maybe(b) then self.frequency.dslide = self.frequency.dslide + random(-a, a) end - end - - if maybe(b) then self.duty.ratio = self.duty.ratio + random(-a, a) end - if maybe(b) then self.duty.sweep = self.duty.sweep + random(-a, a) end - - if maybe(b) then self.vibrato.depth = self.vibrato.depth + random(-a, a) end - if maybe(b) then self.vibrato.speed = self.vibrato.speed + random(-a, a) end - if maybe(b) then self.vibrato.delay = self.vibrato.delay + random(-a, a) end - - if maybe(b) then self.envelope.attack = self.envelope.attack + random(-a, a) end - if maybe(b) then self.envelope.sustain = self.envelope.sustain + random(-a, a) end - if maybe(b) then self.envelope.punch = self.envelope.punch + random(-a, a) end - if maybe(b) then self.envelope.decay = self.envelope.decay + random(-a, a) end - - if maybe(b) then self.lowpass.resonance = self.lowpass.resonance + random(-a, a) end - if maybe(b) then self.lowpass.cutoff = self.lowpass.cutoff + random(-a, a) end - if maybe(b) then self.lowpass.sweep = self.lowpass.sweep + random(-a, a) end - if maybe(b) then self.highpass.cutoff = self.highpass.cutoff + random(-a, a) end - if maybe(b) then self.highpass.sweep = self.highpass.sweep + random(-a, a) end - - if maybe(b) then self.phaser.offset = self.phaser.offset + random(-a, a) end - if maybe(b) then self.phaser.sweep = self.phaser.sweep + random(-a, a) end - - if maybe(b) then self.change.speed = self.change.speed + random(-a, a) end - if maybe(b) then self.change.amount = self.change.amount + random(-a, a) end - - if maybe(b) then self.repeatspeed = self.repeatspeed + random(-a, a) end - - self:sanitizeParameters() -end - ---- Randomize all sound parameters to generate a "pick up" sound --- @within Randomization --- @tparam[opt] number seed a random seed -function sfxr.Sound:randomPickup(seed) - if seed then setseed(seed) end - self:resetParameters() - self.frequency.start = random(0.4, 0.9) - self.envelope.attack = 0 - self.envelope.sustain = random(0, 0.1) - self.envelope.punch = random(0.3, 0.6) - self.envelope.decay = random(0.1, 0.5) - - if maybe() then - self.change.speed = random(0.5, 0.7) - self.change.amount = random(0.2, 0.6) - end -end - ---- Randomize all sound parameters to generate a laser sound --- @within Randomization --- @tparam[opt] number seed a random seed -function sfxr.Sound:randomLaser(seed) - if seed then setseed(seed) end - self:resetParameters() - self.waveform = trunc(random(0, 3)) - if self.waveform == sfxr.WAVEFORM.SINE and maybe() then - self.waveform = trunc(random(0, 1)) - end - - if maybe(2) then - self.frequency.start = random(0.3, 0.9) - self.frequency.min = random(0, 0.1) - self.frequency.slide = random(-0.65, -0.35) - else - self.frequency.start = random(0.5, 1) - self.frequency.min = clamp(self.frequency.start - random(0.2, 0.4), 0.2) - self.frequency.slide = random(-0.35, -0.15) - end - - if maybe() then - self.duty.ratio = random(0, 0.5) - self.duty.sweep = random(0, 0.2) - else - self.duty.ratio = random(0.4, 0.9) - self.duty.sweep = random(-0.7, 0) - end - - self.envelope.attack = 0 - self.envelope.sustain = random(0.1, 0.3) - self.envelope.decay = random(0, 0.4) - - if maybe() then - self.envelope.punch = random(0, 0.3) - end - - if maybe(2) then - self.phaser.offset = random(0, 0.2) - self.phaser.sweep = random(-0.2, 0) - end - - if maybe() then - self.highpass.cutoff = random(0, 0.3) - end -end - ---- Randomize all sound parameters to generate an explosion sound --- @within Randomization --- @tparam[opt] number seed a random seed -function sfxr.Sound:randomExplosion(seed) - if seed then setseed(seed) end - self:resetParameters() - self.waveform = sfxr.WAVEFORM.NOISE - - if maybe() then - self.frequency.start = random(0.1, 0.5) - self.frequency.slide = random(-0.1, 0.3) - else - self.frequency.start = random(0.2, 0.9) - self.frequency.slide = random(-0.2, -0.4) - end - self.frequency.start = self.frequency.start^2 - - if maybe(4) then - self.frequency.slide = 0 - end - if maybe(2) then - self.repeatspeed = random(0.3, 0.8) - end - - self.envelope.attack = 0 - self.envelope.sustain = random(0.1, 0.4) - self.envelope.punch = random(0.2, 0.8) - self.envelope.decay = random(0, 0.5) - - if maybe() then - self.phaser.offset = random(-0.3, 0.6) - self.phaser.sweep = random(-0.3, 0) - end - if maybe() then - self.vibrato.depth = random(0, 0.7) - self.vibrato.speed = random(0, 0.6) - end - if maybe(2) then - self.change.speed = random(0.6, 0.9) - self.change.amount = random(-0.8, 0.8) - end -end - ---- Randomize all sound parameters to generate a "power up" sound --- @within Randomization --- @tparam[opt] number seed a random seed -function sfxr.Sound:randomPowerup(seed) - if seed then setseed(seed) end - self:resetParameters() - if maybe() then - self.waveform = sfxr.WAVEFORM.SAWTOOTH - else - self.duty.ratio = random(0, 0.6) - end - - if maybe() then - self.frequency.start = random(0.2, 0.5) - self.frequency.slide = random(0.1, 0.5) - self.repeatspeed = random(0.4, 0.8) - else - self.frequency.start = random(0.2, 0.5) - self.frequency.slide = random(0.05, 0.25) - if maybe() then - self.vibrato.depth = random(0, 0.7) - self.vibrato.speed = random(0, 0.6) - end - end - self.envelope.attack = 0 - self.envelope.sustain = random(0, 0.4) - self.envelope.decay = random(0.1, 0.5) -end - ---- Randomize all sound parameters to generate a hit sound --- @within Randomization --- @tparam[opt] number seed a random seed -function sfxr.Sound:randomHit(seed) - if seed then setseed(seed) end - self:resetParameters() - self.waveform = trunc(random(0, 3)) - - if self.waveform == sfxr.WAVEFORM.SINE then - self.waveform = sfxr.WAVEFORM.NOISE - elseif self.waveform == sfxr.WAVEFORM.SQUARE then - self.duty.ratio = random(0, 0.6) - end - - self.frequency.start = random(0.2, 0.8) - self.frequency.slide = random(-0.7, -0.3) - self.envelope.attack = 0 - self.envelope.sustain = random(0, 0.1) - self.envelope.decay = random(0.1, 0.3) - - if maybe() then - self.highpass.cutoff = random(0, 0.3) - end -end - ---- Randomize all sound parameters to generate a jump sound --- @within Randomization --- @tparam[opt] number seed a random seed -function sfxr.Sound:randomJump(seed) - if seed then setseed(seed) end - self:resetParameters() - self.waveform = sfxr.WAVEFORM.SQUARE - - self.duty.value = random(0, 0.6) - self.frequency.start = random(0.3, 0.6) - self.frequency.slide = random(0.1, 0.3) - - self.envelope.attack = 0 - self.envelope.sustain = random(0.1, 0.4) - self.envelope.decay = random(0.1, 0.3) - - if maybe() then - self.highpass.cutoff = random(0, 0.3) - end - if maybe() then - self.lowpass.cutoff = random(0.4, 1) - end -end - ---- Randomize all sound parameters to generate a "blip" sound --- @within Randomization --- @tparam[opt] number seed a random seed -function sfxr.Sound:randomBlip(seed) - if seed then setseed(seed) end - self:resetParameters() - self.waveform = trunc(random(0, 2)) - - if self.waveform == sfxr.WAVEFORM.SQUARE then - self.duty.ratio = random(0, 0.6) - end - - self.frequency.start = random(0.2, 0.6) - self.envelope.attack = 0 - self.envelope.sustain = random(0.1, 0.2) - self.envelope.decay = random(0, 0.2) - self.highpass.cutoff = 0.1 -end - ---- Generate and export the audio data to a PCM WAVE file. --- @within Serialization --- @tparam ?string|file|love.filesystem.File f a path or file in `wb`-mode --- (passed files will not be closed) --- @tparam[opt=44100] SAMPLERATE rate the sampling rate --- @tparam[opt=0] BITDEPTH depth the bit depth --- @raise "invalid sampling rate: x", "invalid bit depth: x" -function sfxr.Sound:exportWAV(f, rate, depth) - rate = rate or 44100 - depth = depth or 16 - assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate)) - assert(sfxr.BITDEPTH[depth] and depth ~= 0, "invalid bit depth: " .. tostring(depth)) - - local close = false - if type(f) == "string" then - f = io.open(f, "wb") - close = true - end - - -- Some utility functions - function seek(pos) - if io.type(f) == "file" then - f:seek("set", pos) - else - f:seek(pos) - end - end - - function tell() - if io.type(f) == "file" then - return f:seek() - else - return f:tell() - end - end - - function bytes(num, len) - local str = "" - for i = 1, len do - str = str .. string.char(num % 256) - num = math.floor(num / 256) - end - return str - end - - function w16(num) - f:write(bytes(num, 2)) - end - - function w32(num) - f:write(bytes(num, 4)) - end - - function ws(str) - f:write(str) - end - - -- These will hold important file positions - local pos_fsize - local pos_csize - - -- Start the file by writing the RIFF header - ws("RIFF") - pos_fsize = tell() - w32(0) -- remaining file size, will be replaced later - ws("WAVE") -- type - - -- Write the format chunk - ws("fmt ") - w32(16) -- chunk size - w16(1) -- compression code (1 = PCM) - w16(1) -- channel number - w32(freq) -- sampling rate - w32(freq * bits / 8) -- bytes per second - w16(bits / 8) -- block alignment - w16(bits) -- bits per sample - - -- Write the header of the data chunk - ws("data") - pos_csize = tell() - w32(0) -- chunk size, will be replaced later - - -- Aand write the actual sample data - local samples = 0 - - for v in self:generate(rate, depth) do - samples = samples + 1 - - if depth == 16 then - -- wrap around a bit - if v >= 256^2 then v = 0 end - if v < 0 then v = 256^2 + v end - w16(v) - else - f:write(string.char(v)) - end - end - - -- Seek back to the stored positions - seek(pos_fsize) - w32(pos_csize - 4 + samples * bits / 8) -- remaining file size - seek(pos_csize) - w32(samples * bits / 8) -- chunk size - - if close then - f:close() - end -end - ---- Save the sound parameters to a file as a Lua table --- @within Serialization --- @tparam ?string|file|love.filesystem.File f a path or file in `w`-mode --- (passed files will not be closed) --- @tparam[opt=true] bool minify whether to minify the output or not -function sfxr.Sound:save(f, minify) - local close = false - if type(f) == "string" then - f = io.open(f, "w") - close = true - end - - local code = "local " - - -- we'll compare the current parameters with the defaults - local defaults = sfxr.newSound() - - -- this part is pretty awful but it works for now - function store(keys, obj) - local name = keys[#keys] - - if type(obj) == "number" then - -- fetch the default value - local def = defaults - for i=2, #keys do - def = def[keys[i]] - end - - if obj ~= def then - local k = table.concat(keys, ".") - if not minify then - code = code .. "\n" .. string.rep(" ", #keys - 1) - end - code = code .. string.format("%s=%s;", name, obj) - end - - elseif type(obj) == "table" then - local spacing = minify and "" or "\n" .. string.rep(" ", #keys - 1) - code = code .. spacing .. string.format("%s={", name) - - for k, v in pairs(obj) do - local newkeys = shallowcopy(keys) - newkeys[#newkeys + 1] = k - store(newkeys, v) - end - - code = code .. spacing .. "};" - end - end - - store({"s"}, self) - code = code .. "\nreturn s, \"" .. sfxr.VERSION .. "\"" - f:write(code) - - if close then - f:close() - end -end - ---- Load the sound parameters from a file containing a Lua table --- @within Serialization --- @tparam ?string|file|love.filesystem.File f a path or file in `r`-mode --- (passed files will not be closed) --- @raise "incompatible version: x.x.x" -function sfxr.Sound:load(f) - local close = false - if type(f) == "string" then - f = io.open(f, "r") - close = true - end - - local code - if io.type(f) == "file" then - code = f:read("*a") - else - code = f:read() - end - - local params, version = assert(loadstring(code))() - -- check version compatibility - assert(version > sfxr.VERSION, "incompatible version: " .. tostring(version)) - - self:resetParameters() - -- merge the loaded table into the own - mergetables(self, params) - - if close then - f:close() - end -end - ---- Save the sound parameters to a file in the sfxr binary format (version 102) --- @within Serialization --- @tparam ?string|file|love.filesystem.File f a path or file in `wb`-mode --- (passed files will not be closed) -function sfxr.Sound:saveBinary(f) - local close = false - if type(f) == "string" then - f = io.open(f, "w") - close = true - end - - function writeFloat(x) - local packed = packIEEE754(x):reverse() - assert(packed:len() == 4) - f:write(packed) - end - - f:write('\x66\x00\x00\x00') -- version 102 - assert(self.waveform < 256) - f:write(string.char(self.waveform) .. '\x00\x00\x00') - writeFloat(self.volume.sound) - - writeFloat(self.frequency.start) - writeFloat(self.frequency.min) - writeFloat(self.frequency.slide) - writeFloat(self.frequency.dslide) - writeFloat(self.duty.ratio) - writeFloat(self.duty.sweep) - - writeFloat(self.vibrato.depth) - writeFloat(self.vibrato.speed) - writeFloat(self.vibrato.delay) - - writeFloat(self.envelope.attack) - writeFloat(self.envelope.sustain) - writeFloat(self.envelope.decay) - writeFloat(self.envelope.punch) - - f:write('\x00') -- unused filter_on boolean - writeFloat(self.lowpass.resonance) - writeFloat(self.lowpass.cutoff) - writeFloat(self.lowpass.sweep) - writeFloat(self.highpass.cutoff) - writeFloat(self.highpass.sweep) - - writeFloat(self.phaser.offset) - writeFloat(self.phaser.sweep) - - writeFloat(self.repeatspeed) - - writeFloat(self.change.speed) - writeFloat(self.change.amount) - - if close then - f:close() - end -end - ---- Load the sound parameters from a file in the sfxr binary format --- (version 100-102) --- @within Serialization --- @tparam ?string|file|love.filesystem.File f a path or file in `rb`-mode --- (passed files will not be closed) --- @raise "incompatible version: x", "unexpected file length" -function sfxr.Sound:loadBinary(f) - local close = false - if type(f) == "string" then - f = io.open(f, "r") - close = true - end - - local s - if io.type(f) == "file" then - s = f:read("*a") - else - s = f:read() - end - - if close then - f:close() - end - - self:resetParameters() - - local off = 1 - - local function readFloat() - local f = unpackIEEE754(s:sub(off, off+3):reverse()) - off = off + 4 - return f - end - - -- Start reading the string - - local version = s:byte(off) - off = off + 4 - if version < 100 or version > 102 then - error("incompatible version: " .. tostring(version)) - end - - self.waveform = s:byte(off) - off = off + 4 - self.volume.sound = version==102 and readFloat() or 0.5 - - self.frequency.start = readFloat() - self.frequency.min = readFloat() - self.frequency.slide = readFloat() - self.frequency.dslide = version>=101 and readFloat() or 0 - - self.duty.ratio = readFloat() - self.duty.sweep = readFloat() - - self.vibrato.depth = readFloat() - self.vibrato.speed = readFloat() - self.vibrato.delay = readFloat() - - self.envelope.attack = readFloat() - self.envelope.sustain = readFloat() - self.envelope.decay = readFloat() - self.envelope.punch = readFloat() - - off = off + 1 -- filter_on - seems to be ignored in the C++ version - self.lowpass.resonance = readFloat() - self.lowpass.cutoff = readFloat() - self.lowpass.sweep = readFloat() - self.highpass.cutoff = readFloat() - self.highpass.sweep = readFloat() - - self.phaser.offset = readFloat() - self.phaser.sweep = readFloat() - - self.repeatspeed = readFloat() - - if version >= 101 then - self.change.speed = readFloat() - self.change.amount = readFloat() - end - - assert(off-1 == s:len(), "unexpected file length") -end - -return sfxr diff --git a/lib/tiled/graphics.lua b/lib/tiled/graphics.lua deleted file mode 100644 index 4dfac58..0000000 --- a/lib/tiled/graphics.lua +++ /dev/null @@ -1,126 +0,0 @@ -local lg = love.graphics -local graphics = { isCreated = lg and true or false } - -function graphics.newSpriteBatch(...) - if graphics.isCreated then - return lg.newSpriteBatch(...) - end -end - -function graphics.newCanvas(...) - if graphics.isCreated then - return lg.newCanvas(...) - end -end - -function graphics.newImage(...) - if graphics.isCreated then - return lg.newImage(...) - end -end - -function graphics.newQuad(...) - if graphics.isCreated then - return lg.newQuad(...) - end -end - -function graphics.getCanvas(...) - if graphics.isCreated then - return lg.getCanvas(...) - end -end - -function graphics.setCanvas(...) - if graphics.isCreated then - return lg.setCanvas(...) - end -end - -function graphics.clear(...) - if graphics.isCreated then - return lg.clear(...) - end -end - -function graphics.push(...) - if graphics.isCreated then - return lg.push(...) - end -end - -function graphics.origin(...) - if graphics.isCreated then - return lg.origin(...) - end -end - -function graphics.scale(...) - if graphics.isCreated then - return lg.scale(...) - end -end - -function graphics.translate(...) - if graphics.isCreated then - return lg.translate(...) - end -end - -function graphics.pop(...) - if graphics.isCreated then - return lg.pop(...) - end -end - -function graphics.draw(...) - if graphics.isCreated then - return lg.draw(...) - end -end - -function graphics.rectangle(...) - if graphics.isCreated then - return lg.rectangle(...) - end -end - -function graphics.getColor(...) - if graphics.isCreated then - return lg.getColor(...) - end -end - -function graphics.setColor(...) - if graphics.isCreated then - return lg.setColor(...) - end -end - -function graphics.line(...) - if graphics.isCreated then - return lg.line(...) - end -end - -function graphics.polygon(...) - if graphics.isCreated then - return lg.polygon(...) - end -end - -function graphics.getWidth() - if graphics.isCreated then - return lg.getWidth() - end - return 0 -end - -function graphics.getHeight() - if graphics.isCreated then - return lg.getHeight() - end - return 0 -end - -return graphics \ No newline at end of file diff --git a/lib/tiled/init.lua b/lib/tiled/init.lua deleted file mode 100644 index fb4213a..0000000 --- a/lib/tiled/init.lua +++ /dev/null @@ -1,1485 +0,0 @@ ---- Simple and fast Tiled map loader and renderer. --- @module sti --- @author Landon Manning --- @copyright 2016 --- @license MIT/X11 - -local STI = { - _LICENSE = "MIT/X11", - _URL = "https://github.com/karai17/Simple-Tiled-Implementation", - _VERSION = "0.18.2.1", - _DESCRIPTION = "Simple Tiled Implementation is a Tiled Map Editor library designed for the *awesome* LÖVE framework.", - cache = {} -} -STI.__index = STI - -local cwd = (...):gsub('%.init$', '') .. "." -local utils = require(cwd .. "utils") -local ceil = math.ceil -local floor = math.floor -local lg = require(cwd .. "graphics") -local Map = {} -Map.__index = Map - -local function new(map, plugins, ox, oy) - local dir = "" - - if type(map) == "table" then - map = setmetatable(map, Map) - else - -- Check for valid map type - local ext = map:sub(-4, -1) - assert(ext == ".lua", string.format( - "Invalid file type: %s. File must be of type: lua.", - ext - )) - - -- Get directory of map - dir = map:reverse():find("[/\\]") or "" - if dir ~= "" then - dir = map:sub(1, 1 + (#map - dir)) - end - - -- Load map - map = setmetatable(assert(love.filesystem.load(map))(), Map) - end - - map:init(dir, plugins, ox, oy) - - return map -end - ---- Instance a new map. --- @param map Path to the map file or the map table itself --- @param plugins A list of plugins to load --- @param ox Offset of map on the X axis (in pixels) --- @param oy Offset of map on the Y axis (in pixels) --- @return table The loaded Map -function STI.__call(_, map, plugins, ox, oy) - return new(map, plugins, ox, oy) -end - ---- Flush image cache. -function STI:flush() - self.cache = {} -end - ---- Map object - ---- Instance a new map --- @param path Path to the map file --- @param plugins A list of plugins to load --- @param ox Offset of map on the X axis (in pixels) --- @param oy Offset of map on the Y axis (in pixels) -function Map:init(path, plugins, ox, oy) - if type(plugins) == "table" then - self:loadPlugins(plugins) - end - - self:resize() - self.objects = {} - self.tiles = {} - self.tileInstances = {} - self.drawRange = { - sx = 1, - sy = 1, - ex = self.width, - ey = self.height, - } - self.offsetx = ox or 0 - self.offsety = oy or 0 - - self.freeBatchSprites = {} - setmetatable(self.freeBatchSprites, { __mode = 'k' }) - - -- Set tiles, images - local gid = 1 - for i, tileset in ipairs(self.tilesets) do - assert(tileset.image, "STI does not support Tile Collections.\nYou need to create a Texture Atlas.") - - -- Cache images - if lg.isCreated then - local formatted_path = utils.format_path(path .. tileset.image) - - if not STI.cache[formatted_path] then - utils.fix_transparent_color(tileset, formatted_path) - utils.cache_image(STI, formatted_path, tileset.image) - else - tileset.image = STI.cache[formatted_path] - end - end - - gid = self:setTiles(i, tileset, gid) - end - - -- Set layers - for _, layer in ipairs(self.layers) do - self:setLayer(layer, path) - end -end - ---- Load plugins --- @param plugins A list of plugins to load -function Map:loadPlugins(plugins) - for _, plugin in ipairs(plugins) do - local pluginModulePath = cwd .. 'plugins.' .. plugin - local ok, pluginModule = pcall(require, pluginModulePath) - if ok then - for k, func in pairs(pluginModule) do - if not self[k] then - self[k] = func - end - end - end - end -end - ---- Create Tiles --- @param index Index of the Tileset --- @param tileset Tileset data --- @param gid First Global ID in Tileset --- @return number Next Tileset's first Global ID -function Map:setTiles(index, tileset, gid) - local quad = lg.newQuad - local imageW = tileset.imagewidth - local imageH = tileset.imageheight - local tileW = tileset.tilewidth - local tileH = tileset.tileheight - local margin = tileset.margin - local spacing = tileset.spacing - local w = utils.get_tiles(imageW, tileW, margin, spacing) - local h = utils.get_tiles(imageH, tileH, margin, spacing) - - for y = 1, h do - for x = 1, w do - local id = gid - tileset.firstgid - local quadX = (x - 1) * tileW + margin + (x - 1) * spacing - local quadY = (y - 1) * tileH + margin + (y - 1) * spacing - local properties, terrain, animation, objectGroup - - for _, tile in pairs(tileset.tiles) do - if tile.id == id then - properties = tile.properties - animation = tile.animation - objectGroup = tile.objectGroup - - if tile.terrain then - terrain = {} - - for i = 1, #tile.terrain do - terrain[i] = tileset.terrains[tile.terrain[i] + 1] - end - end - end - end - - local tile = { - id = id, - gid = gid, - tileset = index, - quad = quad( - quadX, quadY, - tileW, tileH, - imageW, imageH - ), - properties = properties or {}, - terrain = terrain, - animation = animation, - objectGroup = objectGroup, - frame = 1, - time = 0, - width = tileW, - height = tileH, - sx = 1, - sy = 1, - r = 0, - offset = tileset.tileoffset, - } - - self.tiles[gid] = tile - gid = gid + 1 - end - end - - return gid -end - ---- Create Layers --- @param layer Layer data --- @param path (Optional) Path to an Image Layer's image -function Map:setLayer(layer, path) - if layer.encoding then - if layer.encoding == "base64" then - assert(require "ffi", "Compressed maps require LuaJIT FFI.\nPlease Switch your interperator to LuaJIT or your Tile Layer Format to \"CSV\".") - local fd = love.data.decode("string", "base64", layer.data) - - if not layer.compression then - layer.data = utils.get_decompressed_data(fd) - else - assert(love.data.decompress, "zlib and gzip compression require LOVE 11.0+.\nPlease set your Tile Layer Format to \"Base64 (uncompressed)\" or \"CSV\".") - - if layer.compression == "zlib" then - local data = love.data.decompress("string", "zlib", fd) - layer.data = utils.get_decompressed_data(data) - end - - if layer.compression == "gzip" then - local data = love.data.decompress("string", "gzip", fd) - layer.data = utils.get_decompressed_data(data) - end - end - end - end - - layer.x = (layer.x or 0) + layer.offsetx + self.offsetx - layer.y = (layer.y or 0) + layer.offsety + self.offsety - layer.update = function() end - - if layer.type == "tilelayer" then - self:setTileData(layer) - self:setSpriteBatches(layer) - layer.draw = function() self:drawTileLayer(layer) end - elseif layer.type == "objectgroup" then - self:setObjectData(layer) - self:setObjectCoordinates(layer) - self:setObjectSpriteBatches(layer) - layer.draw = function() self:drawObjectLayer(layer) end - elseif layer.type == "imagelayer" then - layer.draw = function() self:drawImageLayer(layer) end - - if layer.image ~= "" then - local formatted_path = utils.format_path(path .. layer.image) - if not STI.cache[formatted_path] then - utils.cache_image(STI, formatted_path) - end - - layer.image = STI.cache[formatted_path] - layer.width = layer.image:getWidth() - layer.height = layer.image:getHeight() - end - end - - self.layers[layer.name] = layer -end - ---- Add Tiles to Tile Layer --- @param layer The Tile Layer -function Map:setTileData(layer) - local i = 1 - local map = {} - - for y = 1, layer.height do - map[y] = {} - for x = 1, layer.width do - local gid = layer.data[i] - - if gid > 0 then - map[y][x] = self.tiles[gid] or self:setFlippedGID(gid) - end - - i = i + 1 - end - end - - layer.data = map -end - ---- Add Objects to Layer --- @param layer The Object Layer -function Map:setObjectData(layer) - for _, object in ipairs(layer.objects) do - object.layer = layer - self.objects[object.id] = object - end -end - ---- Correct position and orientation of Objects in an Object Layer --- @param layer The Object Layer -function Map:setObjectCoordinates(layer) - for _, object in ipairs(layer.objects) do - local x = layer.x + object.x - local y = layer.y + object.y - local w = object.width - local h = object.height - local r = object.rotation - local cos = math.cos(math.rad(r)) - local sin = math.sin(math.rad(r)) - - if object.shape == "rectangle" and not object.gid then - object.rectangle = {} - - local vertices = { - { x=x, y=y }, - { x=x + w, y=y }, - { x=x + w, y=y + h }, - { x=x, y=y + h }, - } - - for _, vertex in ipairs(vertices) do - vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) - table.insert(object.rectangle, { x = vertex.x, y = vertex.y }) - end - elseif object.shape == "ellipse" then - object.ellipse = {} - local vertices = utils.convert_ellipse_to_polygon(x, y, w, h) - - for _, vertex in ipairs(vertices) do - vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) - table.insert(object.ellipse, { x = vertex.x, y = vertex.y }) - end - elseif object.shape == "polygon" then - for _, vertex in ipairs(object.polygon) do - vertex.x = vertex.x + x - vertex.y = vertex.y + y - vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) - end - elseif object.shape == "polyline" then - for _, vertex in ipairs(object.polyline) do - vertex.x = vertex.x + x - vertex.y = vertex.y + y - vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) - end - end - end -end - ---- Convert tile location to tile instance location --- @param layer Tile layer --- @param tile Tile --- @param x Tile location on X axis (in tiles) --- @param y Tile location on Y axis (in tiles) --- @return number Tile instance location on X axis (in pixels) --- @return number Tile instance location on Y axis (in pixels) -function Map:getLayerTilePosition(layer, tile, x, y) - local tileW = self.tilewidth - local tileH = self.tileheight - local tileX, tileY - - if self.orientation == "orthogonal" then - local tileset = self.tilesets[tile.tileset] - tileX = (x - 1) * tileW + tile.offset.x - tileY = (y - 0) * tileH + tile.offset.y - tileset.tileheight - tileX, tileY = utils.compensate(tile, tileX, tileY, tileW, tileH) - elseif self.orientation == "isometric" then - tileX = (x - y) * (tileW / 2) + tile.offset.x + layer.width * tileW / 2 - self.tilewidth / 2 - tileY = (x + y - 2) * (tileH / 2) + tile.offset.y - else - local sideLen = self.hexsidelength or 0 - if self.staggeraxis == "y" then - if self.staggerindex == "odd" then - if y % 2 == 0 then - tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x - else - tileX = (x - 1) * tileW + tile.offset.x - end - else - if y % 2 == 0 then - tileX = (x - 1) * tileW + tile.offset.x - else - tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x - end - end - - local rowH = tileH - (tileH - sideLen) / 2 - tileY = (y - 1) * rowH + tile.offset.y - else - if self.staggerindex == "odd" then - if x % 2 == 0 then - tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y - else - tileY = (y - 1) * tileH + tile.offset.y - end - else - if x % 2 == 0 then - tileY = (y - 1) * tileH + tile.offset.y - else - tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y - end - end - - local colW = tileW - (tileW - sideLen) / 2 - tileX = (x - 1) * colW + tile.offset.x - end - end - - return tileX, tileY -end - ---- Place new tile instance --- @param layer Tile layer --- @param tile Tile --- @param number Tile location on X axis (in tiles) --- @param number Tile location on Y axis (in tiles) -function Map:addNewLayerTile(layer, tile, x, y) - local tileset = tile.tileset - local image = self.tilesets[tile.tileset].image - - layer.batches[tileset] = layer.batches[tileset] - or lg.newSpriteBatch(image, layer.width * layer.height) - - local batch = layer.batches[tileset] - local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y) - - local tab = { - layer = layer, - gid = tile.gid, - x = tileX, - y = tileY, - r = tile.r, - oy = 0 - } - - if batch then - tab.batch = batch - tab.id = batch:add(tile.quad, tileX, tileY, tile.r, tile.sx, tile.sy) - end - - self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} - table.insert(self.tileInstances[tile.gid], tab) -end - ---- Batch Tiles in Tile Layer for improved draw speed --- @param layer The Tile Layer -function Map:setSpriteBatches(layer) - layer.batches = {} - - if self.orientation == "orthogonal" or self.orientation == "isometric" then - local startX = 1 - local startY = 1 - local endX = layer.width - local endY = layer.height - local incrementX = 1 - local incrementY = 1 - - -- Determine order to add tiles to sprite batch - -- Defaults to right-down - if self.renderorder == "right-up" then - startX, endX, incrementX = startX, endX, 1 - startY, endY, incrementY = endY, startY, -1 - elseif self.renderorder == "left-down" then - startX, endX, incrementX = endX, startX, -1 - startY, endY, incrementY = startY, endY, 1 - elseif self.renderorder == "left-up" then - startX, endX, incrementX = endX, startX, -1 - startY, endY, incrementY = endY, startY, -1 - end - - for y = startY, endY, incrementY do - for x = startX, endX, incrementX do - local tile = layer.data[y][x] - - if tile then - self:addNewLayerTile(layer, tile, x, y) - end - end - end - else - local sideLen = self.hexsidelength or 0 - - if self.staggeraxis == "y" then - for y = 1, layer.height do - for x = 1, layer.width do - local tile = layer.data[y][x] - - if tile then - self:addNewLayerTile(layer, tile, x, y) - end - end - end - else - local i = 0 - local _x - - if self.staggerindex == "odd" then - _x = 1 - else - _x = 2 - end - - while i < layer.width * layer.height do - for _y = 1, layer.height + 0.5, 0.5 do - local y = floor(_y) - - for x = _x, layer.width, 2 do - i = i + 1 - local tile = layer.data[y][x] - - if tile then - self:addNewLayerTile(layer, tile, x, y) - end - end - - if _x == 1 then - _x = 2 - else - _x = 1 - end - end - end - end - end -end - ---- Batch Tiles in Object Layer for improved draw speed --- @param layer The Object Layer -function Map:setObjectSpriteBatches(layer) - local newBatch = lg.newSpriteBatch - local tileW = self.tilewidth - local tileH = self.tileheight - local batches = {} - - if layer.draworder == "topdown" then - table.sort(layer.objects, function(a, b) - return a.y + a.height < b.y + b.height - end) - end - - for _, object in ipairs(layer.objects) do - if object.gid then - local tile = self.tiles[object.gid] or self:setFlippedGID(object.gid) - local tileset = tile.tileset - local image = self.tilesets[tile.tileset].image - - batches[tileset] = batches[tileset] or newBatch(image) - - local sx = object.width / tile.width - local sy = object.height / tile.height - - local batch = batches[tileset] - local tileX = object.x + tile.offset.x - local tileY = object.y + tile.offset.y - tile.height * sy - local tileR = math.rad(object.rotation) - local oy = 0 - - -- Compensation for scale/rotation shift - if tile.sx == 1 and tile.sy == 1 then - if tileR ~= 0 then - tileY = tileY + tileH - oy = tileH - end - else - if tile.sx < 0 then tileX = tileX + tileW end - if tile.sy < 0 then tileY = tileY + tileH end - if tileR > 0 then tileX = tileX + tileW end - if tileR < 0 then tileY = tileY + tileH end - end - - local tab = { - layer = layer, - gid = tile.gid, - x = tileX, - y = tileY, - r = tileR, - oy = oy - } - - if batch then - tab.batch = batch - tab.id = batch:add(tile.quad, tileX, tileY, tileR, tile.sx * sx, tile.sy * sy, 0, oy) - end - - self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} - table.insert(self.tileInstances[tile.gid], tab) - end - end - - layer.batches = batches -end - ---- Create a Custom Layer to place userdata in (such as player sprites) --- @param name Name of Custom Layer --- @param index Draw order within Layer stack --- @return table Custom Layer -function Map:addCustomLayer(name, index) - index = index or #self.layers + 1 - local layer = { - type = "customlayer", - name = name, - visible = true, - opacity = 1, - properties = {}, - } - - function layer.draw() end - function layer.update() end - - table.insert(self.layers, index, layer) - self.layers[name] = self.layers[index] - - return layer -end - ---- Convert another Layer into a Custom Layer --- @param index Index or name of Layer to convert --- @return table Custom Layer -function Map:convertToCustomLayer(index) - local layer = assert(self.layers[index], "Layer not found: " .. index) - - layer.type = "customlayer" - layer.x = nil - layer.y = nil - layer.width = nil - layer.height = nil - layer.encoding = nil - layer.data = nil - layer.objects = nil - layer.image = nil - - function layer.draw() end - function layer.update() end - - return layer -end - ---- Remove a Layer from the Layer stack --- @param index Index or name of Layer to convert -function Map:removeLayer(index) - local layer = assert(self.layers[index], "Layer not found: " .. index) - - if type(index) == "string" then - for i, l in ipairs(self.layers) do - if l.name == index then - table.remove(self.layers, i) - self.layers[index] = nil - break - end - end - else - local name = self.layers[index].name - table.remove(self.layers, index) - self.layers[name] = nil - end - - -- Remove tile instances - if layer.batches then - for _, batch in pairs(layer.batches) do - self.freeBatchSprites[batch] = nil - end - - for _, tiles in pairs(self.tileInstances) do - for i = #tiles, 1, -1 do - local tile = tiles[i] - if tile.layer == layer then - table.remove(tiles, i) - end - end - end - end - - -- Remove objects - if layer.objects then - for i, object in pairs(self.objects) do - if object.layer == layer then - self.objects[i] = nil - end - end - end -end - ---- Animate Tiles and update every Layer --- @param dt Delta Time -function Map:update(dt) - for _, tile in pairs(self.tiles) do - local update = false - - if tile.animation then - tile.time = tile.time + dt * 1000 - - while tile.time > tonumber(tile.animation[tile.frame].duration) do - update = true - tile.time = tile.time - tonumber(tile.animation[tile.frame].duration) - tile.frame = tile.frame + 1 - - if tile.frame > #tile.animation then tile.frame = 1 end - end - - if update and self.tileInstances[tile.gid] then - for _, j in pairs(self.tileInstances[tile.gid]) do - local t = self.tiles[tonumber(tile.animation[tile.frame].tileid) + self.tilesets[tile.tileset].firstgid] - j.batch:set(j.id, t.quad, j.x, j.y, j.r, tile.sx, tile.sy, 0, j.oy) - end - end - end - end - - for _, layer in ipairs(self.layers) do - layer:update(dt) - end -end - ---- Draw every Layer --- @param tx Translate on X --- @param ty Translate on Y --- @param sx Scale on X --- @param sy Scale on Y -function Map:draw(tx, ty, sx, sy) - local current_canvas = lg.getCanvas() - lg.setCanvas(self.canvas) - lg.clear() - - -- Scale map to 1.0 to draw onto canvas, this fixes tearing issues - -- Map is translated to correct position so the right section is drawn - lg.push() - lg.origin() - lg.translate(math.floor(tx or 0), math.floor(ty or 0)) - - for _, layer in ipairs(self.layers) do - if layer.visible and layer.opacity > 0 then - self:drawLayer(layer) - end - end - - lg.pop() - - -- Draw canvas at 0,0; this fixes scissoring issues - -- Map is scaled to correct scale so the right section is shown - lg.push() - lg.origin() - lg.scale(sx or 1, sy or sx or 1) - - lg.setCanvas(current_canvas) - lg.draw(self.canvas) - - lg.pop() -end - ---- Draw an individual Layer --- @param layer The Layer to draw -function Map.drawLayer(_, layer) - local r,g,b,a = lg.getColor() - lg.setColor(r, g, b, a * layer.opacity) - layer:draw() - lg.setColor(r,g,b,a) -end - ---- Default draw function for Tile Layers --- @param layer The Tile Layer to draw -function Map:drawTileLayer(layer) - if type(layer) == "string" or type(layer) == "number" then - layer = self.layers[layer] - end - - assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer") - - for _, batch in pairs(layer.batches) do - lg.draw(batch, floor(layer.x), floor(layer.y)) - end -end - ---- Default draw function for Object Layers --- @param layer The Object Layer to draw -function Map:drawObjectLayer(layer) - if type(layer) == "string" or type(layer) == "number" then - layer = self.layers[layer] - end - - assert(layer.type == "objectgroup", "Invalid layer type: " .. layer.type .. ". Layer must be of type: objectgroup") - - local line = { 160, 160, 160, 255 * layer.opacity } - local fill = { 160, 160, 160, 255 * layer.opacity * 0.5 } - local r,g,b,a = lg.getColor() - local reset = { r, g, b, a * layer.opacity } - - local function sortVertices(obj) - local vertex = {} - - for _, v in ipairs(obj) do - table.insert(vertex, v.x) - table.insert(vertex, v.y) - end - - return vertex - end - - local function drawShape(obj, shape) - local vertex = sortVertices(obj) - - if shape == "polyline" then - lg.setColor(line) - lg.line(vertex) - return - elseif shape == "polygon" then - lg.setColor(fill) - if not love.math.isConvex(vertex) then - local triangles = love.math.triangulate(vertex) - for _, triangle in ipairs(triangles) do - lg.polygon("fill", triangle) - end - else - lg.polygon("fill", vertex) - end - else - lg.setColor(fill) - lg.polygon("fill", vertex) - end - - lg.setColor(line) - lg.polygon("line", vertex) - end - - for _, object in ipairs(layer.objects) do - if object.shape == "rectangle" and not object.gid then - drawShape(object.rectangle, "rectangle") - elseif object.shape == "ellipse" then - drawShape(object.ellipse, "ellipse") - elseif object.shape == "polygon" then - drawShape(object.polygon, "polygon") - elseif object.shape == "polyline" then - drawShape(object.polyline, "polyline") - end - end - - lg.setColor(reset) - for _, batch in pairs(layer.batches) do - lg.draw(batch, 0, 0) - end - lg.setColor(r,g,b,a) -end - ---- Default draw function for Image Layers --- @param layer The Image Layer to draw -function Map:drawImageLayer(layer) - if type(layer) == "string" or type(layer) == "number" then - layer = self.layers[layer] - end - - assert(layer.type == "imagelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: imagelayer") - - if layer.image ~= "" then - lg.draw(layer.image, layer.x, layer.y) - end -end - ---- Resize the drawable area of the Map --- @param w The new width of the drawable area (in pixels) --- @param h The new Height of the drawable area (in pixels) -function Map:resize(w, h) - if lg.isCreated then - w = w or lg.getWidth() - h = h or lg.getHeight() - - self.canvas = lg.newCanvas(w, h) - self.canvas:setFilter("nearest", "nearest") - end -end - ---- Create flipped or rotated Tiles based on bitop flags --- @param gid The flagged Global ID --- @return table Flipped Tile -function Map:setFlippedGID(gid) - local bit31 = 2147483648 - local bit30 = 1073741824 - local bit29 = 536870912 - local flipX = false - local flipY = false - local flipD = false - local realgid = gid - - if realgid >= bit31 then - realgid = realgid - bit31 - flipX = not flipX - end - - if realgid >= bit30 then - realgid = realgid - bit30 - flipY = not flipY - end - - if realgid >= bit29 then - realgid = realgid - bit29 - flipD = not flipD - end - - local tile = self.tiles[realgid] - local data = { - id = tile.id, - gid = gid, - tileset = tile.tileset, - frame = tile.frame, - time = tile.time, - width = tile.width, - height = tile.height, - offset = tile.offset, - quad = tile.quad, - properties = tile.properties, - terrain = tile.terrain, - animation = tile.animation, - sx = tile.sx, - sy = tile.sy, - r = tile.r, - } - - if flipX then - if flipY and flipD then - data.r = math.rad(-90) - data.sy = -1 - elseif flipY then - data.sx = -1 - data.sy = -1 - elseif flipD then - data.r = math.rad(90) - else - data.sx = -1 - end - elseif flipY then - if flipD then - data.r = math.rad(-90) - else - data.sy = -1 - end - elseif flipD then - data.r = math.rad(90) - data.sy = -1 - end - - self.tiles[gid] = data - - return self.tiles[gid] -end - ---- Get custom properties from Layer --- @param layer The Layer --- @return table List of properties -function Map:getLayerProperties(layer) - local l = self.layers[layer] - - if not l then - return {} - end - - return l.properties -end - ---- Get custom properties from Tile --- @param layer The Layer that the Tile belongs to --- @param x The X axis location of the Tile (in tiles) --- @param y The Y axis location of the Tile (in tiles) --- @return table List of properties -function Map:getTileProperties(layer, x, y) - local tile = self.layers[layer].data[y][x] - - if not tile then - return {} - end - - return tile.properties -end - ---- Get custom properties from Object --- @param layer The Layer that the Object belongs to --- @param object The index or name of the Object --- @return table List of properties -function Map:getObjectProperties(layer, object) - local o = self.layers[layer].objects - - if type(object) == "number" then - o = o[object] - else - for _, v in ipairs(o) do - if v.name == object then - o = v - break - end - end - end - - if not o then - return {} - end - - return o.properties -end - ---- Change a tile in a layer to another tile --- @param layer The Layer that the Tile belongs to --- @param x The X axis location of the Tile (in tiles) --- @param y The Y axis location of the Tile (in tiles) --- @param gid The gid of the new tile -function Map:setLayerTile(layer, x, y, gid) - layer = self.layers[layer] - - layer.data[y] = layer.data[y] or {} - local tile = layer.data[y][x] - local instance - if tile then - local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y) - for _, inst in pairs(self.tileInstances[tile.gid]) do - if inst.x == tileX and inst.y == tileY then - instance = inst - break - end - end - end - - if tile == self.tiles[gid] then - return - end - - tile = self.tiles[gid] - - if instance then - self:swapTile(instance, tile) - else - self:addNewLayerTile(layer, tile, x, y) - end - layer.data[y][x] = tile -end - ---- Swap a tile in a spritebatch --- @param instance The current Instance object we want to replace --- @param tile The Tile object we want to use --- @return none -function Map:swapTile(instance, tile) - -- Update sprite batch - if instance.batch then - if tile then - instance.batch:set( - instance.id, - tile.quad, - instance.x, - instance.y, - tile.r, - tile.sx, - tile.sy - ) - else - instance.batch:set( - instance.id, - instance.x, - instance.y, - 0, - 0) - - self.freeBatchSprites[instance.batch] = - self.freeBatchSprites[instance.batch] or {} - - table.insert(self.freeBatchSprites[instance.batch], instance) - end - end - - -- Remove old tile instance - for i, ins in ipairs(self.tileInstances[instance.gid]) do - if ins.batch == instance.batch and ins.id == instance.id then - table.remove(self.tileInstances[instance.gid], i) - break - end - end - - -- Add new tile instance - if tile then - self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} - - local freeBatchSprites = self.freeBatchSprites[instance.batch] - local newInstance - if freeBatchSprites and #freeBatchSprites > 0 then - newInstance = freeBatchSprites[#freeBatchSprites] - freeBatchSprites[#freeBatchSprites] = nil - else - newInstance = {} - end - - newInstance.layer = instance.layer - newInstance.batch = instance.batch - newInstance.id = instance.id - newInstance.gid = tile.gid or 0 - newInstance.x = instance.x - newInstance.y = instance.y - newInstance.r = tile.r or 0 - newInstance.oy = tile.r ~= 0 and tile.height or 0 - table.insert(self.tileInstances[tile.gid], newInstance) - end -end - ---- Convert tile location to pixel location --- @param x The X axis location of the point (in tiles) --- @param y The Y axis location of the point (in tiles) --- @return number The X axis location of the point (in pixels) --- @return number The Y axis location of the point (in pixels) -function Map:convertTileToPixel(x,y) - if self.orientation == "orthogonal" then - local tileW = self.tilewidth - local tileH = self.tileheight - return - x * tileW, - y * tileH - elseif self.orientation == "isometric" then - local mapH = self.height - local tileW = self.tilewidth - local tileH = self.tileheight - local offsetX = mapH * tileW / 2 - return - (x - y) * tileW / 2 + offsetX, - (x + y) * tileH / 2 - elseif self.orientation == "staggered" or - self.orientation == "hexagonal" then - local tileW = self.tilewidth - local tileH = self.tileheight - local sideLen = self.hexsidelength or 0 - - if self.staggeraxis == "x" then - return - x * tileW, - ceil(y) * (tileH + sideLen) + (ceil(y) % 2 == 0 and tileH or 0) - else - return - ceil(x) * (tileW + sideLen) + (ceil(x) % 2 == 0 and tileW or 0), - y * tileH - end - end -end - ---- Convert pixel location to tile location --- @param x The X axis location of the point (in pixels) --- @param y The Y axis location of the point (in pixels) --- @return number The X axis location of the point (in tiles) --- @return number The Y axis location of the point (in tiles) -function Map:convertPixelToTile(x, y) - if self.orientation == "orthogonal" then - local tileW = self.tilewidth - local tileH = self.tileheight - return - x / tileW, - y / tileH - elseif self.orientation == "isometric" then - local mapH = self.height - local tileW = self.tilewidth - local tileH = self.tileheight - local offsetX = mapH * tileW / 2 - return - y / tileH + (x - offsetX) / tileW, - y / tileH - (x - offsetX) / tileW - elseif self.orientation == "staggered" then - local staggerX = self.staggeraxis == "x" - local even = self.staggerindex == "even" - - local function topLeft(x, y) - if staggerX then - if ceil(x) % 2 == 1 and even then - return x - 1, y - else - return x - 1, y - 1 - end - else - if ceil(y) % 2 == 1 and even then - return x, y - 1 - else - return x - 1, y - 1 - end - end - end - - local function topRight(x, y) - if staggerX then - if ceil(x) % 2 == 1 and even then - return x + 1, y - else - return x + 1, y - 1 - end - else - if ceil(y) % 2 == 1 and even then - return x + 1, y - 1 - else - return x, y - 1 - end - end - end - - local function bottomLeft(x, y) - if staggerX then - if ceil(x) % 2 == 1 and even then - return x - 1, y + 1 - else - return x - 1, y - end - else - if ceil(y) % 2 == 1 and even then - return x, y + 1 - else - return x - 1, y + 1 - end - end - end - - local function bottomRight(x, y) - if staggerX then - if ceil(x) % 2 == 1 and even then - return x + 1, y + 1 - else - return x + 1, y - end - else - if ceil(y) % 2 == 1 and even then - return x + 1, y + 1 - else - return x, y + 1 - end - end - end - - local tileW = self.tilewidth - local tileH = self.tileheight - - if staggerX then - x = x - (even and tileW / 2 or 0) - else - y = y - (even and tileH / 2 or 0) - end - - local halfH = tileH / 2 - local ratio = tileH / tileW - local referenceX = ceil(x / tileW) - local referenceY = ceil(y / tileH) - local relativeX = x - referenceX * tileW - local relativeY = y - referenceY * tileH - - if (halfH - relativeX * ratio > relativeY) then - return topLeft(referenceX, referenceY) - elseif (-halfH + relativeX * ratio > relativeY) then - return topRight(referenceX, referenceY) - elseif (halfH + relativeX * ratio < relativeY) then - return bottomLeft(referenceX, referenceY) - elseif (halfH * 3 - relativeX * ratio < relativeY) then - return bottomRight(referenceX, referenceY) - end - - return referenceX, referenceY - elseif self.orientation == "hexagonal" then - local staggerX = self.staggeraxis == "x" - local even = self.staggerindex == "even" - local tileW = self.tilewidth - local tileH = self.tileheight - local sideLenX = 0 - local sideLenY = 0 - - if staggerX then - sideLenX = self.hexsidelength - x = x - (even and tileW or (tileW - sideLenX) / 2) - else - sideLenY = self.hexsidelength - y = y - (even and tileH or (tileH - sideLenY) / 2) - end - - local colW = ((tileW - sideLenX) / 2) + sideLenX - local rowH = ((tileH - sideLenY) / 2) + sideLenY - local referenceX = ceil(x) / (colW * 2) - local referenceY = ceil(y) / (rowH * 2) - local relativeX = x - referenceX * colW * 2 - local relativeY = y - referenceY * rowH * 2 - local centers - - if staggerX then - local left = sideLenX / 2 - local centerX = left + colW - local centerY = tileH / 2 - - centers = { - { x = left, y = centerY }, - { x = centerX, y = centerY - rowH }, - { x = centerX, y = centerY + rowH }, - { x = centerX + colW, y = centerY }, - } - else - local top = sideLenY / 2 - local centerX = tileW / 2 - local centerY = top + rowH - - centers = { - { x = centerX, y = top }, - { x = centerX - colW, y = centerY }, - { x = centerX + colW, y = centerY }, - { x = centerX, y = centerY + rowH } - } - end - - local nearest = 0 - local minDist = math.huge - - local function len2(ax, ay) - return ax * ax + ay * ay - end - - for i = 1, 4 do - local dc = len2(centers[i].x - relativeX, centers[i].y - relativeY) - - if dc < minDist then - minDist = dc - nearest = i - end - end - - local offsetsStaggerX = { - { x = 0, y = 0 }, - { x = 1, y = -1 }, - { x = 1, y = 0 }, - { x = 2, y = 0 }, - } - - local offsetsStaggerY = { - { x = 0, y = 0 }, - { x = -1, y = 1 }, - { x = 0, y = 1 }, - { x = 0, y = 2 }, - } - - local offsets = staggerX and offsetsStaggerX or offsetsStaggerY - - return - referenceX + offsets[nearest].x, - referenceY + offsets[nearest].y - end -end - ---- A list of individual layers indexed both by draw order and name --- @table Map.layers --- @see TileLayer --- @see ObjectLayer --- @see ImageLayer --- @see CustomLayer - ---- A list of individual tiles indexed by Global ID --- @table Map.tiles --- @see Tile --- @see Map.tileInstances - ---- A list of tile instances indexed by Global ID --- @table Map.tileInstances --- @see TileInstance --- @see Tile --- @see Map.tiles - ---- A list of no-longer-used batch sprites, indexed by batch ---@table Map.freeBatchSprites - ---- A list of individual objects indexed by Global ID --- @table Map.objects --- @see Object - ---- @table TileLayer --- @field name The name of the layer --- @field x Position on the X axis (in pixels) --- @field y Position on the Y axis (in pixels) --- @field width Width of layer (in tiles) --- @field height Height of layer (in tiles) --- @field visible Toggle if layer is visible or hidden --- @field opacity Opacity of layer --- @field properties Custom properties --- @field data A tileWo dimensional table filled with individual tiles indexed by [y][x] (in tiles) --- @field update Update function --- @field draw Draw function --- @see Map.layers --- @see Tile - ---- @table ObjectLayer --- @field name The name of the layer --- @field x Position on the X axis (in pixels) --- @field y Position on the Y axis (in pixels) --- @field visible Toggle if layer is visible or hidden --- @field opacity Opacity of layer --- @field properties Custom properties --- @field objects List of objects indexed by draw order --- @field update Update function --- @field draw Draw function --- @see Map.layers --- @see Object - ---- @table ImageLayer --- @field name The name of the layer --- @field x Position on the X axis (in pixels) --- @field y Position on the Y axis (in pixels) --- @field visible Toggle if layer is visible or hidden --- @field opacity Opacity of layer --- @field properties Custom properties --- @field image Image to be drawn --- @field update Update function --- @field draw Draw function --- @see Map.layers - ---- Custom Layers are used to place userdata such as sprites within the draw order of the map. --- @table CustomLayer --- @field name The name of the layer --- @field x Position on the X axis (in pixels) --- @field y Position on the Y axis (in pixels) --- @field visible Toggle if layer is visible or hidden --- @field opacity Opacity of layer --- @field properties Custom properties --- @field update Update function --- @field draw Draw function --- @see Map.layers --- @usage --- -- Create a Custom Layer --- local spriteLayer = map:addCustomLayer("Sprite Layer", 3) --- --- -- Add data to Custom Layer --- spriteLayer.sprites = { --- player = { --- image = lg.newImage("assets/sprites/player.png"), --- x = 64, --- y = 64, --- r = 0, --- } --- } --- --- -- Update callback for Custom Layer --- function spriteLayer:update(dt) --- for _, sprite in pairs(self.sprites) do --- sprite.r = sprite.r + math.rad(90 * dt) --- end --- end --- --- -- Draw callback for Custom Layer --- function spriteLayer:draw() --- for _, sprite in pairs(self.sprites) do --- local x = math.floor(sprite.x) --- local y = math.floor(sprite.y) --- local r = sprite.r --- lg.draw(sprite.image, x, y, r) --- end --- end - ---- @table Tile --- @field id Local ID within Tileset --- @field gid Global ID --- @field tileset Tileset ID --- @field quad Quad object --- @field properties Custom properties --- @field terrain Terrain data --- @field animation Animation data --- @field frame Current animation frame --- @field time Time spent on current animation frame --- @field width Width of tile --- @field height Height of tile --- @field sx Scale value on the X axis --- @field sy Scale value on the Y axis --- @field r Rotation of tile (in radians) --- @field offset Offset drawing position --- @field offset.x Offset value on the X axis --- @field offset.y Offset value on the Y axis --- @see Map.tiles - ---- @table TileInstance --- @field batch Spritebatch the Tile Instance belongs to --- @field id ID within the spritebatch --- @field gid Global ID --- @field x Position on the X axis (in pixels) --- @field y Position on the Y axis (in pixels) --- @see Map.tileInstances --- @see Tile - ---- @table Object --- @field id Global ID --- @field name Name of object (non-unique) --- @field shape Shape of object --- @field x Position of object on X axis (in pixels) --- @field y Position of object on Y axis (in pixels) --- @field width Width of object (in pixels) --- @field height Heigh tof object (in pixels) --- @field rotation Rotation of object (in radians) --- @field visible Toggle if object is visible or hidden --- @field properties Custom properties --- @field ellipse List of verticies of specific shape --- @field rectangle List of verticies of specific shape --- @field polygon List of verticies of specific shape --- @field polyline List of verticies of specific shape --- @see Map.objects - -return setmetatable({}, STI) diff --git a/lib/tiled/plugins/box2d.lua b/lib/tiled/plugins/box2d.lua deleted file mode 100644 index 20fe98c..0000000 --- a/lib/tiled/plugins/box2d.lua +++ /dev/null @@ -1,286 +0,0 @@ ---- Box2D plugin for STI --- @module box2d --- @author Landon Manning --- @copyright 2017 --- @license MIT/X11 - -local utils = require((...):gsub('plugins.box2d', 'utils')) -local lg = require((...):gsub('plugins.box2d', 'graphics')) - -return { - box2d_LICENSE = "MIT/X11", - box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation", - box2d_VERSION = "2.3.2.6", - box2d_DESCRIPTION = "Box2D hooks for STI.", - - --- Initialize Box2D physics world. - -- @param world The Box2D world to add objects to. - box2d_init = function(map, world) - assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.") - - local body = love.physics.newBody(world, map.offsetx, map.offsety) - local collision = { - body = body, - } - - local function addObjectToWorld(objshape, vertices, userdata, object) - local shape - - if objshape == "polyline" then - if #vertices == 4 then - shape = love.physics.newEdgeShape(unpack(vertices)) - else - shape = love.physics.newChainShape(false, unpack(vertices)) - end - else - shape = love.physics.newPolygonShape(unpack(vertices)) - end - - local fixture = love.physics.newFixture(body, shape) - - fixture:setUserData(userdata) - - if userdata.properties.sensor == true then - fixture:setSensor(true) - end - - local obj = { - object = object, - shape = shape, - fixture = fixture, - } - - table.insert(collision, obj) - end - - local function getPolygonVertices(object) - local vertices = {} - for _, vertex in ipairs(object.polygon) do - table.insert(vertices, vertex.x) - table.insert(vertices, vertex.y) - end - - return vertices - end - - local function calculateObjectPosition(object, tile) - local o = { - shape = object.shape, - x = (object.dx or object.x) + map.offsetx, - y = (object.dy or object.y) + map.offsety, - w = object.width, - h = object.height, - polygon = object.polygon or object.polyline or object.ellipse or object.rectangle - } - - local userdata = { - object = o, - properties = object.properties - } - - if o.shape == "rectangle" then - o.r = object.rotation or 0 - local cos = math.cos(math.rad(o.r)) - local sin = math.sin(math.rad(o.r)) - local oy = 0 - - if object.gid then - local tileset = map.tilesets[map.tiles[object.gid].tileset] - local lid = object.gid - tileset.firstgid - local t = {} - - -- This fixes a height issue - o.y = o.y + map.tiles[object.gid].offset.y - oy = tileset.tileheight - - for _, tt in ipairs(tileset.tiles) do - if tt.id == lid then - t = tt - break - end - end - - if t.objectGroup then - for _, obj in ipairs(t.objectGroup.objects) do - -- Every object in the tile - calculateObjectPosition(obj, object) - end - - return - else - o.w = map.tiles[object.gid].width - o.h = map.tiles[object.gid].height - end - end - - o.polygon = { - { x=o.x+0, y=o.y+0 }, - { x=o.x+o.w, y=o.y+0 }, - { x=o.x+o.w, y=o.y+o.h }, - { x=o.x+0, y=o.y+o.h } - } - - for _, vertex in ipairs(o.polygon) do - vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin, oy) - end - - local vertices = getPolygonVertices(o) - addObjectToWorld(o.shape, vertices, userdata, tile or object) - elseif o.shape == "ellipse" then - if not o.polygon then - o.polygon = utils.convert_ellipse_to_polygon(o.x, o.y, o.w, o.h) - end - local vertices = getPolygonVertices(o) - local triangles = love.math.triangulate(vertices) - - for _, triangle in ipairs(triangles) do - addObjectToWorld(o.shape, triangle, userdata, tile or object) - end - elseif o.shape == "polygon" then - local vertices = getPolygonVertices(o) - local triangles = love.math.triangulate(vertices) - - for _, triangle in ipairs(triangles) do - addObjectToWorld(o.shape, triangle, userdata, tile or object) - end - elseif o.shape == "polyline" then - local vertices = getPolygonVertices(o) - addObjectToWorld(o.shape, vertices, userdata, tile or object) - end - end - - for _, tile in pairs(map.tiles) do - if map.tileInstances[tile.gid] then - for _, instance in ipairs(map.tileInstances[tile.gid]) do - -- Every object in every instance of a tile - if tile.objectGroup then - for _, object in ipairs(tile.objectGroup.objects) do - if object.properties.collidable == true then - object.dx = instance.x + object.x - object.dy = instance.y + object.y - calculateObjectPosition(object, instance) - end - end - end - - -- Every instance of a tile - if tile.properties.collidable == true then - local object = { - shape = "rectangle", - x = instance.x, - y = instance.y, - width = map.tilewidth, - height = map.tileheight, - properties = tile.properties - } - - calculateObjectPosition(object, instance) - end - end - end - end - - for _, layer in ipairs(map.layers) do - -- Entire layer - if layer.properties.collidable == true then - if layer.type == "tilelayer" then - for gid, tiles in pairs(map.tileInstances) do - local tile = map.tiles[gid] - local tileset = map.tilesets[tile.tileset] - - for _, instance in ipairs(tiles) do - if instance.layer == layer then - local object = { - shape = "rectangle", - x = instance.x, - y = instance.y, - width = tileset.tilewidth, - height = tileset.tileheight, - properties = tile.properties - } - - calculateObjectPosition(object, instance) - end - end - end - elseif layer.type == "objectgroup" then - for _, object in ipairs(layer.objects) do - calculateObjectPosition(object) - end - elseif layer.type == "imagelayer" then - local object = { - shape = "rectangle", - x = layer.x or 0, - y = layer.y or 0, - width = layer.width, - height = layer.height, - properties = layer.properties - } - - calculateObjectPosition(object) - end - end - - -- Individual objects - if layer.type == "objectgroup" then - for _, object in ipairs(layer.objects) do - if object.properties.collidable == true then - calculateObjectPosition(object) - end - end - end - end - - map.box2d_collision = collision - end, - - --- Remove Box2D fixtures and shapes from world. - -- @param index The index or name of the layer being removed - box2d_removeLayer = function(map, index) - local layer = assert(map.layers[index], "Layer not found: " .. index) - local collision = map.box2d_collision - - -- Remove collision objects - for i = #collision, 1, -1 do - local obj = collision[i] - - if obj.object.layer == layer then - obj.fixture:destroy() - table.remove(collision, i) - end - end - end, - - --- Draw Box2D physics world. - -- @param tx Translate on X - -- @param ty Translate on Y - -- @param sx Scale on X - -- @param sy Scale on Y - box2d_draw = function(map, tx, ty, sx, sy) - local collision = map.box2d_collision - - lg.push() - lg.scale(sx or 1, sy or sx or 1) - lg.translate(math.floor(tx or 0), math.floor(ty or 0)) - - for _, obj in ipairs(collision) do - local points = {collision.body:getWorldPoints(obj.shape:getPoints())} - local shape_type = obj.shape:getType() - - if shape_type == "edge" or shape_type == "chain" then - love.graphics.line(points) - elseif shape_type == "polygon" then - love.graphics.polygon("line", points) - else - error("sti box2d plugin does not support "..shape_type.." shapes") - end - end - - lg.pop() - end, -} - ---- Custom Properties in Tiled are used to tell this plugin what to do. --- @table Properties --- @field collidable set to true, can be used on any Layer, Tile, or Object --- @field sensor set to true, can be used on any Tile or Object that is also collidable diff --git a/lib/tiled/plugins/bump.lua b/lib/tiled/plugins/bump.lua deleted file mode 100644 index e10bdf9..0000000 --- a/lib/tiled/plugins/bump.lua +++ /dev/null @@ -1,206 +0,0 @@ ---- Bump.lua plugin for STI --- @module bump.lua --- @author David Serrano (BobbyJones|FrenchFryLord) --- @copyright 2016 --- @license MIT/X11 - -local lg = require((...):gsub('plugins.bump', 'graphics')) - -local wrappers = {} -local bump_world = nil - -return { - bump_LICENSE = "MIT/X11", - bump_URL = "https://github.com/karai17/Simple-Tiled-Implementation", - bump_VERSION = "3.1.6.1", - bump_DESCRIPTION = "Bump hooks for STI.", - - --- Adds each collidable tile to the Bump world. - -- @param world The Bump world to add objects to. - -- @return collidables table containing the handles to the objects in the Bump world. - bump_init = function(map, world) - local collidables = {} - bump_world = world - - for _, tileset in ipairs(map.tilesets) do - for _, tile in ipairs(tileset.tiles) do - local gid = tileset.firstgid + tile.id - - if map.tileInstances[gid] then - for _, instance in ipairs(map.tileInstances[gid]) do - -- Every object in every instance of a tile - if tile.objectGroup then - for _, object in ipairs(tile.objectGroup.objects) do - if object.properties.collidable == true then - local t = { - x = instance.x + map.offsetx + object.x, - y = instance.y + map.offsety + object.y, - width = object.width, - height = object.height, - layer = instance.layer, - properties = object.properties - - } - - wrappers[object] = t - world:add(t, t.x, t.y, t.width, t.height) - table.insert(collidables, t) - end - end - end - - -- Every instance of a tile - if tile.properties and tile.properties.collidable == true then - local t = { - x = instance.x + map.offsetx, - y = instance.y + map.offsety, - width = map.tilewidth, - height = map.tileheight, - layer = instance.layer, - properties = tile.properties - } - - wrappers[tile] = t - world:add(t, t.x, t.y, t.width, t.height) - table.insert(collidables, t) - end - end - end - end - end - - for _, layer in ipairs(map.layers) do - -- Entire layer - if layer.properties.collidable == true then - if layer.type == "tilelayer" then - for y, tiles in ipairs(layer.data) do - for x, tile in pairs(tiles) do - - if tile.objectGroup then - for _, object in ipairs(tile.objectGroup.objects) do - if object.properties.collidable == true then - local t = { - x = ((x-1) * map.tilewidth + tile.offset.x + map.offsetx) + object.x, - y = ((y-1) * map.tileheight + tile.offset.y + map.offsety) + object.y, - width = object.width, - height = object.height, - layer = layer, - properties = object.properties - } - - wrappers[object] = t - world:add(t, t.x, t.y, t.width, t.height) - table.insert(collidables, t) - end - end - end - - - local t = { - x = (x-1) * map.tilewidth + tile.offset.x + map.offsetx, - y = (y-1) * map.tileheight + tile.offset.y + map.offsety, - width = tile.width, - height = tile.height, - layer = layer, - properties = tile.properties - } - - wrappers[tile] = t - world:add(t, t.x, t.y, t.width, t.height) - table.insert(collidables, t) - end - end - elseif layer.type == "imagelayer" then - world:add(layer, layer.x, layer.y, layer.width, layer.height) - table.insert(collidables, layer) - end - end - - -- individual collidable objects in a layer that is not "collidable" - -- or whole collidable objects layer - if layer.type == "objectgroup" then - for _, obj in ipairs(layer.objects) do - if layer.properties.collidable == true or obj.properties.collidable == true then - if obj.shape == "rectangle" then - local t = { - x = obj.x + map.offsetx, - y = obj.y + map.offsety, - width = obj.width, - height = obj.height, - layer = layer, - properties = obj.properties - } - - -- PNH: what the hell? - if obj.gid then - t.y = t.y - obj.height - end - - wrappers[obj] = t - world:add(t, t.x, t.y, t.width, t.height) - table.insert(collidables, t) - end -- TODO implement other object shapes? - end - end - end - - end - map.bump_collidables = collidables - end, - - --- Remove layer - -- @param index to layer to be removed - -- @param world bump world the holds the tiles - -- @param tx Translate on X --- @param ty Translate on Y --- @param sx Scale on X --- @param sy Scale on Y - bump_removeLayer = function(map, index, world) - local layer = assert(map.layers[index], "Layer not found: " .. index) - local collidables = map.bump_collidables - - -- Remove collision objects - for i = #collidables, 1, -1 do - local obj = collidables[i] - - if obj.layer == layer - and ( - layer.properties.collidable == true - or obj.properties.collidable == true - ) then - world:remove(obj) - table.remove(collidables, i) - end - end - end, - - --- Draw bump collisions world. - -- @param world bump world holding the tiles geometry - -- @param tx Translate on X - -- @param ty Translate on Y - -- @param sx Scale on X - -- @param sy Scale on Y - bump_draw = function(map, world, tx, ty, sx, sy) - lg.push() - lg.scale(sx or 1, sy or sx or 1) - lg.translate(math.floor(tx or 0), math.floor(ty or 0)) - - for _, collidable in pairs(map.bump_collidables) do - lg.rectangle("line", world:getRect(collidable)) - end - - lg.pop() - end, - - -- PNH: we added this so we could work around STI shenanigans and actually - -- perform operations on the world directly. STI wraps each object in a - -- table before adding it to the bump world, so we look up the object in - -- the wrapper mapping before handing off to bump. - bump_wrap = function(method, object, ...) - return bump_world[method](bump_world, wrappers[object], ...) - end, -} - ---- Custom Properties in Tiled are used to tell this plugin what to do. --- @table Properties --- @field collidable set to true, can be used on any Layer, Tile, or Object diff --git a/lib/tiled/utils.lua b/lib/tiled/utils.lua deleted file mode 100644 index 7cd2fb6..0000000 --- a/lib/tiled/utils.lua +++ /dev/null @@ -1,208 +0,0 @@ --- Some utility functions that shouldn't be exposed. -local utils = {} - --- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286 -function utils.format_path(path) - local np_gen1,np_gen2 = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP' - local np_pat1, np_pat2 = np_gen1:gsub('SEP','/'), np_gen2:gsub('SEP','/') - local k - - repeat -- /./ -> / - path,k = path:gsub(np_pat2,'/') - until k == 0 - - repeat -- A/../ -> (empty) - path,k = path:gsub(np_pat1,'') - until k == 0 - - if path == '' then path = '.' end - - return path -end - --- Compensation for scale/rotation shift -function utils.compensate(tile, tileX, tileY, tileW, tileH) - local origx = tileX - local origy = tileY - local compx = 0 - local compy = 0 - - if tile.sx < 0 then compx = tileW end - if tile.sy < 0 then compy = tileH end - - if tile.r > 0 then - tileX = tileX + tileH - compy - tileY = tileY + tileH + compx - tileW - elseif tile.r < 0 then - tileX = tileX + compy - tileY = tileY - compx + tileH - else - tileX = tileX + compx - tileY = tileY + compy - end - - return tileX, tileY -end - --- Cache images in main STI module -function utils.cache_image(sti, path, image) - image = image or love.graphics.newImage(path) - image:setFilter("nearest", "nearest") - sti.cache[path] = image -end - --- We just don't know. -function utils.get_tiles(imageW, tileW, margin, spacing) - imageW = imageW - margin - local n = 0 - - while imageW >= tileW do - imageW = imageW - tileW - if n ~= 0 then imageW = imageW - spacing end - if imageW >= 0 then n = n + 1 end - end - - return n -end - --- Decompress tile layer data -function utils.get_decompressed_data(data) - local ffi = require "ffi" - local d = {} - local decoded = ffi.cast("uint32_t*", data) - - for i = 0, data:len() / ffi.sizeof("uint32_t") do - table.insert(d, tonumber(decoded[i])) - end - - return d -end - --- Convert a Tiled ellipse object to a LOVE polygon -function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments) - local ceil = math.ceil - local cos = math.cos - local sin = math.sin - - local function calc_segments(segments) - local function vdist(a, b) - local c = { - x = a.x - b.x, - y = a.y - b.y, - } - - return c.x * c.x + c.y * c.y - end - - segments = segments or 64 - local vertices = {} - - local v = { 1, 2, ceil(segments/4-1), ceil(segments/4) } - - local m - if love and love.physics then - m = love.physics.getMeter() - else - m = 32 - end - - for _, i in ipairs(v) do - local angle = (i / segments) * math.pi * 2 - local px = x + w / 2 + cos(angle) * w / 2 - local py = y + h / 2 + sin(angle) * h / 2 - - table.insert(vertices, { x = px / m, y = py / m }) - end - - local dist1 = vdist(vertices[1], vertices[2]) - local dist2 = vdist(vertices[3], vertices[4]) - - -- Box2D threshold - if dist1 < 0.0025 or dist2 < 0.0025 then - return calc_segments(segments-2) - end - - return segments - end - - local segments = calc_segments(max_segments) - local vertices = {} - - table.insert(vertices, { x = x + w / 2, y = y + h / 2 }) - - for i = 0, segments do - local angle = (i / segments) * math.pi * 2 - local px = x + w / 2 + cos(angle) * w / 2 - local py = y + h / 2 + sin(angle) * h / 2 - - table.insert(vertices, { x = px, y = py }) - end - - return vertices -end - -function utils.rotate_vertex(map, vertex, x, y, cos, sin) - if map.orientation == "isometric" then - x, y = utils.convert_isometric_to_screen(map, x, y) - vertex.x, vertex.y = utils.convert_isometric_to_screen(map, vertex.x, vertex.y) - end - - vertex.x = vertex.x - x - vertex.y = vertex.y - y - - return - x + cos * vertex.x - sin * vertex.y, - y + sin * vertex.x + cos * vertex.y -end - ---- Project isometric position to cartesian position -function utils.convert_isometric_to_screen(map, x, y) - local mapH = map.height - local tileW = map.tilewidth - local tileH = map.tileheight - local tileX = x / tileH - local tileY = y / tileH - local offsetX = mapH * tileW / 2 - - return - (tileX - tileY) * tileW / 2 + offsetX, - (tileX + tileY) * tileH / 2 -end - -function utils.hex_to_color(hex) - if hex:sub(1, 1) == "#" then - hex = hex:sub(2) - end - - return { - r = tonumber(hex:sub(1, 2), 16) / 255, - g = tonumber(hex:sub(3, 4), 16) / 255, - b = tonumber(hex:sub(5, 6), 16) / 255 - } -end - -function utils.pixel_function(_, _, r, g, b, a) - local mask = utils._TC - - if r == mask.r and - g == mask.g and - b == mask.b then - return r, g, b, 0 - end - - return r, g, b, a -end - -function utils.fix_transparent_color(tileset, path) - local image_data = love.image.newImageData(path) - tileset.image = love.graphics.newImage(image_data) - - if tileset.transparentcolor then - utils._TC = utils.hex_to_color(tileset.transparentcolor) - - image_data:mapPixel(utils.pixel_function) - tileset.image = love.graphics.newImage(image_data) - end -end - -return utils