├── doc └── pathfinding0.png ├── .gitignore ├── README.md ├── tests ├── map.bin ├── test.lua └── console.lua ├── map2D.lua ├── pqueue.lua ├── map.lua ├── mapSolver.lua └── serpent.lua /doc/pathfinding0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulik/pathfinding/HEAD/doc/pathfinding0.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.sdf 3 | *.suo 4 | *.opensdf 5 | *.user 6 | *.dll 7 | *.exe 8 | std*.txt 9 | debug.txt 10 | *.log 11 | *.orig 12 | *.bak 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A* pathfinding algorithm in Lua 2 | ============== 3 | 4 | General usage: 5 | -------------- 6 | 7 | ```lua 8 | local mapGenerator = require 'map' 9 | local mapSolver = require 'mapSolver' 10 | 11 | local map = mapGenerator { 12 | width = 25, 13 | height = 25, 14 | entry = {x = 15, y = 7}, 15 | exit = {x = 23, y = 11}, 16 | } 17 | 18 | -- generate a map 19 | map.generate() 20 | 21 | -- prepare map pathfinding solver 22 | local solver = mapSolver(map) 23 | local solve = solver.solve() 24 | local validPath 25 | 26 | local solver = mapSolver(map) 27 | local solve = solver.solve() 28 | 29 | for i=1,10000 do 30 | local result, p0 = solve() 31 | if result then 32 | validPath = p0 33 | break 34 | end 35 | end 36 | 37 | -- validPath will contain points of path from entry to exit point 38 | ``` 39 | 40 | Tests: 41 | ------ 42 | 43 | Run a test with (Win32 environment only due console handler): 44 | 45 | ``` 46 | luajit tests/test.lua 47 | ``` 48 | 49 | Screenshots: 50 | ------------ 51 | ![alt text](https://github.com/soulik/pathfinding/raw/master/doc/pathfinding0.png "Sample map with pathfinding") -------------------------------------------------------------------------------- /tests/map.bin: -------------------------------------------------------------------------------- 1 | do local _={width=25,height=25,exit={y=15,x=23},finishOnExit=false,entry={y=7,x=15},map="do local _={[477]=0,[235]=0,[480]=0,[240]=1,[796]=1,[798]=1,[505]=1,[504]=0,[507]=1,[506]=1,[509]=0,[508]=0,[511]=0,[510]=0,[388]=0,[389]=1,[390]=1,[391]=1,[757]=1,[386]=0,[387]=0,[196]=1,[197]=1,[539]=1,[541]=0,[543]=0,[195]=1,[721]=0,[719]=1,[717]=1,[414]=0,[415]=0,[573]=1,[575]=1,[419]=1,[418]=1,[417]=0,[416]=0,[685]=1,[421]=0,[420]=1,[683]=0,[215]=1,[679]=1,[214]=0,[922]=0,[443]=0,[645]=1,[647]=0,[643]=1,[446]=0,[445]=0,[444]=0,[218]=1,[879]=0,[334]=1,[335]=1,[332]=0,[610]=1,[608]=1,[606]=1,[839]=0,[338]=1,[336]=1,[337]=1,[576]=0,[359]=0,[358]=0,[365]=0,[364]=1,[361]=1,[360]=1,[363]=1,[362]=1,[176]=1,[177]=1,[760]=0,[758]=1,[756]=1,[965]=1,[880]=0,[799]=0,[722]=1,[641]=1,[536]=0,[538]=1,[540]=1,[259]=1,[260]=1,[261]=1,[262]=1,[263]=1,[280]=1,[281]=0,[282]=0,[283]=1,[284]=1,[285]=1,[716]=1,[287]=1,[158]=1,[570]=1,[572]=1,[574]=1,[759]=0,[612]=0,[648]=1,[239]=1,[837]=1,[797]=1,[720]=1,[684]=0,[836]=1,[680]=1,[682]=1,[194]=0,[678]=1,[392]=0,[577]=0,[795]=1,[718]=1,[681]=1,[755]=1,[921]=0,[311]=1,[310]=1,[309]=1,[308]=1,[307]=1,[306]=0,[305]=1,[644]=1,[646]=1,[157]=0,[286]=1,[642]=1,[571]=1,[450]=0,[537]=1,[312]=1,[448]=1,[449]=1,[175]=0,[140]=0,[447]=0,[258]=0,[878]=1,[217]=1,[331]=0,[333]=1,[611]=0,[609]=1,[607]=1,[605]=1,[542]=0,[393]=1,[216]=1,[237]=1,[838]=1,[236]=0,[257]=1,[474]=1,[475]=1,[238]=1,[473]=0,[478]=0,[479]=0,[476]=0};return _;end"};return _;end -------------------------------------------------------------------------------- /map2D.lua: -------------------------------------------------------------------------------- 1 | local serpent = require 'serpent' 2 | 3 | return function(defaultValue) 4 | local t = {} 5 | 6 | --[[ 7 | Cantor pairing function 8 | \pi(k_1,k_2) := \frac{1}{2}(k_1 + k_2)(k_1 + k_2 + 1)+k_2. 9 | --]] 10 | local function primitiveCantorPair(k1, k2) 11 | return 0.5 * (k1 + k2) * ((k1 + k2) + 1) + k2 12 | end 13 | 14 | local function generalizedCantorPair(arg) 15 | if #arg == 2 then 16 | return primitiveCantorPair(unpack(arg)) 17 | elseif #arg > 2 then 18 | return primitiveCantorPair(generalizedCantorPair(arg), table.remove(arg)) 19 | else 20 | error('Cantor pairing function need at least 2 arguments!') 21 | end 22 | end 23 | 24 | local function cantorPair(...) 25 | return generalizedCantorPair({...}) 26 | end 27 | 28 | local load = function(data) 29 | r, t = serpent.load(data) 30 | return r 31 | end 32 | 33 | local save = function() 34 | return serpent.dump(t) 35 | end 36 | 37 | setmetatable(t, { 38 | __index = function(_, k) 39 | if type(k)=="table" then 40 | local i = rawget(t, cantorPair(k[1] or 1, k[2] or 1)) 41 | return i or defaultValue 42 | elseif type(k)=="string" then 43 | if k=="load" then 44 | return load 45 | elseif k=="save" then 46 | return save 47 | end 48 | end 49 | end, 50 | __newindex = function(_, k, v) 51 | if type(k)=="table" then 52 | rawset(t, cantorPair(k[1] or 1, k[2] or 1), v) 53 | else 54 | rawset(t, k, v) 55 | end 56 | end, 57 | }) 58 | 59 | return t 60 | end 61 | -------------------------------------------------------------------------------- /pqueue.lua: -------------------------------------------------------------------------------- 1 | local ti = table.insert 2 | local tr = table.remove 3 | 4 | local tr2 = function(t, v) 5 | for i=1,#t do 6 | if t[i]==v then 7 | tr(t, i) 8 | break 9 | end 10 | end 11 | end 12 | 13 | return function () 14 | local t = {} 15 | 16 | -- a set of elements 17 | local set = {} 18 | -- a set of priorities paired with a elements 19 | local r_set = {} 20 | -- sorted list of priorities 21 | local keys = {} 22 | 23 | -- add element into storage and set its priority and sort keys 24 | local function addKV(k, v) 25 | set[k] = v 26 | if not r_set[v] then 27 | ti(keys, v) 28 | table.sort(keys) 29 | local k0 = {k} 30 | r_set[v] = k0 31 | setmetatable(k0, { 32 | __mode = 'v' 33 | }) 34 | else 35 | ti(r_set[v], k) 36 | end 37 | end 38 | 39 | -- remove element from storage and sort keys 40 | local remove = function(k) 41 | local v = set[k] 42 | local prioritySet = r_set[v] 43 | tr2(prioritySet, k) 44 | if #prioritySet < 1 then 45 | tr2(keys, v) 46 | r_set[v] = nil 47 | table.sort(keys) 48 | set[k] = nil 49 | end 50 | end; t.remove = remove 51 | 52 | -- returns an element with the lowest priority 53 | t.min = function() 54 | local priority = keys[1] 55 | if priority then 56 | return r_set[priority] or {} 57 | else 58 | return {} 59 | end 60 | end 61 | 62 | -- returns an element with the highest priority 63 | t.max = function() 64 | local priority = keys[#keys] 65 | if priority then 66 | return r_set[priority] or {} 67 | else 68 | return {} 69 | end 70 | end 71 | 72 | -- is this queue empty? 73 | t.empty = function() 74 | return #keys < 1 75 | end 76 | 77 | setmetatable(t, { 78 | __index = set, 79 | __newindex = function(t, k, v) 80 | if not set[k] then 81 | -- new item 82 | addKV(k, v) 83 | else 84 | -- existing item 85 | remove(k) 86 | addKV(k, v) 87 | end 88 | end, 89 | }) 90 | 91 | return t 92 | end 93 | -------------------------------------------------------------------------------- /tests/test.lua: -------------------------------------------------------------------------------- 1 | package.path = '../?.lua;tests/?.lua;'..package.path 2 | local mapGenerator = require 'map' 3 | local mapSolver = require 'mapSolver' 4 | local console = require 'console' 5 | local bit = require 'bit' 6 | 7 | do 8 | local generateMap = true 9 | 10 | local map 11 | math.randomseed(os.time()) 12 | 13 | local tt = { 14 | [0] = {string.char(219),0x0057}, -- a wall 15 | [1] = {string.char(177),0x0001}, -- an empty cell 16 | [2] = {string.char(178),0x000A}, -- an empty cell 17 | } 18 | 19 | local con = console.prepare() 20 | os.execute( "cls" ) 21 | 22 | local function drawCell(x, y, cellType) 23 | if tt[cellType] then 24 | con.write(x, y, tt[cellType][1], tt[cellType][2]) 25 | con.write(x+1, y, tt[cellType][1], tt[cellType][2]) 26 | end 27 | end 28 | 29 | local function drawMap() 30 | for position, cell in map.iterator() do 31 | local cellType = cell or 0 32 | local dx, dy = position[1], position[2] 33 | drawCell(dx*2, dy, cellType) 34 | end 35 | end 36 | 37 | local function loadMap(fname) 38 | local f = io.open(fname, 'rb') 39 | if f then 40 | local data = f:read('*a') 41 | f:close() 42 | map.load(data) 43 | end 44 | end 45 | 46 | local function saveMap(fname) 47 | local f = io.open(fname, 'wb') 48 | if f then 49 | f:write(map.save()) 50 | f:close() 51 | end 52 | end 53 | 54 | local blindAttempts = 0 55 | 56 | map = mapGenerator { 57 | width = 25, 58 | height = 25, 59 | entry = {x = 15, y = 7}, 60 | exit = {x = 23, y = 15}, 61 | finishOnExit = false, 62 | } 63 | 64 | if not generateMap then 65 | loadMap('map.bin') 66 | else 67 | map.generate() 68 | saveMap('map.bin') 69 | end 70 | --drawMap() 71 | 72 | local stepsTaken = 0 73 | local solver = mapSolver(map) 74 | local solve = solver.solve() 75 | local validPath 76 | 77 | --local f = io.open('debug.log','w') 78 | 79 | for i=1,10000 do 80 | local result, p0 = solve() 81 | if result then 82 | validPath = p0 83 | break 84 | end 85 | end 86 | 87 | local mapCells = map.map 88 | 89 | for _, p in ipairs(validPath) do 90 | mapCells[p] = 2 91 | end 92 | 93 | drawMap() 94 | 95 | print('Steps', #validPath) 96 | end 97 | -------------------------------------------------------------------------------- /tests/console.lua: -------------------------------------------------------------------------------- 1 | local ffi = require 'ffi' 2 | 3 | local kernel32_h = [[ 4 | typedef unsigned long HANDLE; 5 | typedef unsigned long DWORD; 6 | typedef unsigned short WORD; 7 | typedef short SHORT; 8 | typedef bool BOOL; 9 | 10 | typedef struct { 11 | short X; 12 | short Y; 13 | } COORD; 14 | 15 | HANDLE GetStdHandle(DWORD nStdHandle); 16 | BOOL SetConsoleCursorPosition(HANDLE hConsoleOutput, COORD dwCursorPosition); 17 | 18 | BOOL WriteConsoleOutputCharacterA( 19 | HANDLE hConsoleOutput, 20 | char * lpCharacter, 21 | DWORD nLength, 22 | COORD dwWriteCoord, 23 | DWORD * lpNumberOfCharsWritten 24 | ); 25 | BOOL WriteConsoleOutputAttribute( 26 | HANDLE hConsoleOutput, 27 | const WORD *lpAttribute, 28 | DWORD nLength, 29 | COORD dwWriteCoord, 30 | DWORD * lpNumberOfAttrsWritten 31 | ); 32 | 33 | DWORD GetLastError(void); 34 | 35 | static const int STD_INPUT_HANDLE = ((DWORD)-10); 36 | static const int STD_OUTPUT_HANDLE = ((DWORD)-11); 37 | static const int STD_ERROR_HANDLE = ((DWORD)-12); 38 | ]] 39 | 40 | ffi.cdef(kernel32_h) 41 | local kernel32 = ffi.load('kernel32.dll') 42 | 43 | local STD_INPUT_HANDLE = kernel32.STD_INPUT_HANDLE 44 | local STD_OUTPUT_HANDLE = kernel32.STD_OUTPUT_HANDLE 45 | local STD_ERROR_HANDLE = kernel32.STD_ERROR_HANDLE 46 | 47 | local M = { 48 | prepare = function() 49 | local handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE) 50 | 51 | local out = { 52 | setPosition = function(x, y) 53 | local position = ffi.new('COORD', {x or 0, y or 0}) 54 | kernel32.SetConsoleCursorPosition(handle, position) 55 | end, 56 | write = function(x, y, char, attr) 57 | local position = ffi.new('COORD', {x or 0, y or 0}) 58 | local numWritten = ffi.new('DWORD[1]', 0) 59 | 60 | local character = ffi.new('char[1]', string.byte(char, 1, 1) or 0) 61 | local attribute = ffi.new('WORD[1]', attr or 0) 62 | 63 | assert(kernel32.WriteConsoleOutputAttribute(handle, attribute, 1, position, numWritten) == true) 64 | assert(kernel32.WriteConsoleOutputCharacterA(handle, character, 1, position, numWritten) == true) 65 | end, 66 | getLastError = function() 67 | return kernel32.GetLastError() 68 | end, 69 | } 70 | return out 71 | end, 72 | } 73 | 74 | return M 75 | 76 | -------------------------------------------------------------------------------- /map.lua: -------------------------------------------------------------------------------- 1 | local serpent = require 'serpent' 2 | local bit = require 'bit' 3 | local map2D = require 'map2d' 4 | 5 | return function(def) 6 | local map = def or {} 7 | map.width = def.width or 31 8 | map.height = def.height or 31 9 | map.map = map2D(1) 10 | map.entry = def.entry or {x = 2, y = 2} 11 | map.exit = def.exit or {x = 30, y = 4} 12 | map.finishOnExit = def.finishOnExit or false 13 | 14 | local mapCells = map.map 15 | local sx, sy = map.entry.x, map.entry.y 16 | local fx, fy = map.exit.x, map.exit.y 17 | 18 | local shapes = { 19 | ['c'] = { 20 | w = 8, h = 12, 21 | data = { 22 | 0,0,0,0,0,0,1,1, 23 | 1,1,1,1,0,0,0,1, 24 | 1,1,1,1,1,0,0,0, 25 | 1,1,1,1,1,1,0,0, 26 | 1,1,1,1,1,1,0,0, 27 | 1,1,1,1,1,1,0,0, 28 | 1,1,1,1,1,1,0,0, 29 | 1,1,1,1,1,1,0,0, 30 | 1,1,1,1,1,1,0,0, 31 | 1,1,1,1,1,0,0,0, 32 | 0,0,1,1,0,0,0,1, 33 | 1,0,0,0,0,0,1,1, 34 | } 35 | } 36 | } 37 | 38 | local function pushShape(name, offset, orientation) 39 | local shape = shapes[name] 40 | local data = shape.data 41 | local orientation = orientation or {1, 1} 42 | 43 | for y=0,shape.h-1 do 44 | for x=0,shape.w-1 do 45 | local pos = {offset[1] + x, offset[2] + y} 46 | local sx, sy 47 | if orientation[1]>0 then 48 | sx = math.floor(x*orientation[1]) 49 | else 50 | sx = math.floor(shape.w + x*orientation[1] - 1) 51 | end 52 | if orientation[2]>0 then 53 | sy = math.floor(y*orientation[2]) 54 | else 55 | sy = math.floor(shape.h + y*orientation[2] - 1) 56 | end 57 | 58 | local v = data[sx + sy*shape.w + 1] 59 | if mapCells[pos] ~= 0 then 60 | mapCells[pos] = v 61 | end 62 | end 63 | end 64 | end 65 | 66 | local genMap = coroutine.wrap(function() 67 | pushShape('c', {12, 4}, {1,1}) 68 | pushShape('c', {17, 8}, {-1,1}) 69 | coroutine.yield(1) 70 | end) 71 | 72 | map.iterator = function() 73 | local co = coroutine.create(function() 74 | for y=1,map.height do 75 | for x=1,map.width do 76 | local position = {x, y} 77 | coroutine.yield(position, mapCells[position]) 78 | end 79 | end 80 | coroutine.yield() 81 | end) 82 | return function() 83 | local code, pos, cell = coroutine.resume(co) 84 | return pos, cell 85 | end 86 | end 87 | 88 | map.load = function(data) 89 | local r, t = serpent.load(data) 90 | if r then 91 | map.width = t.width 92 | map.height = t.height 93 | map.entry = t.entry 94 | map.exit = t.exit 95 | map.finishOnExit = t.finishOnExit 96 | map.map.load(t.map) 97 | end 98 | end 99 | 100 | map.save = function() 101 | return serpent.dump({ 102 | map = map.map.save(), 103 | width = map.width, 104 | height = map.height, 105 | entry = map.entry, 106 | exit = map.exit, 107 | finishOnExit = map.finishOnExit, 108 | }) 109 | end 110 | 111 | map.generate = genMap 112 | return map 113 | end -------------------------------------------------------------------------------- /mapSolver.lua: -------------------------------------------------------------------------------- 1 | local bit = require 'bit' 2 | local pqueue = require 'pqueue' 3 | 4 | local function dprint(fmt, ...) 5 | local f = io.open('debug.log','a') 6 | if f then 7 | if #({...}) > 0 then 8 | f:write(fmt:format(...)) 9 | else 10 | f:write(fmt) 11 | end 12 | f:write("\n") 13 | f:close() 14 | end 15 | end 16 | 17 | return function(map) 18 | local solver = {} 19 | local mapCells = map.map 20 | 21 | local solve = function(startingPoint, exitPoint) 22 | local start = startingPoint or {map.entry.x, map.entry.y} 23 | local goal = exitPoint or {map.exit.x, map.exit.y} 24 | 25 | return coroutine.wrap(function() 26 | local pEq = function(p0, p1) 27 | return (p0[1] == p1[1]) and (p0[2] == p1[2]) 28 | end 29 | 30 | local function cantorPair(k1, k2) 31 | return 0.5 * (k1 + k2) * ((k1 + k2) + 1) + k2 32 | end 33 | 34 | local cellCache = {} 35 | 36 | local function queryCell(p) 37 | local cp = cantorPair(p[1], p[2]) 38 | local cell = cellCache[cp] 39 | if not cell then 40 | cell = p 41 | cellCache[cp] = cell 42 | end 43 | return cell 44 | end 45 | 46 | local function storeCell(...) 47 | for _, elm in ipairs {...} do 48 | cellCache[cantorPair(elm[1], elm[2])] = elm 49 | end 50 | end 51 | 52 | local directionSet = { 53 | {0, 1}, 54 | {1, 1}, 55 | {1, 0}, 56 | {1, -1}, 57 | {0, -1}, 58 | {-1, -1}, 59 | {-1, 0}, 60 | {-1, 1}, 61 | } 62 | 63 | local neighbours = function(p0, testFn) 64 | return coroutine.wrap(function() 65 | local list = {} 66 | for _, direction in ipairs(directionSet) do 67 | local _p1 = {p0[1] + direction[1], p0[2] + direction[2]} 68 | if type(testFn)=='function' then 69 | if testFn(_p1) then 70 | coroutine.yield(queryCell(_p1)) 71 | end 72 | else 73 | coroutine.yield(queryCell(_p1)) 74 | end 75 | end 76 | coroutine.yield() 77 | end) 78 | end 79 | 80 | local function heuristicCostEstimate(p0, p1) 81 | return math.abs(p0[1] - p1[1]) + math.abs(p0[2] - p1[2]) 82 | end 83 | 84 | local function cost(p0, p1) 85 | local cell = mapCells[p1] 86 | 87 | if cell == 0 then 88 | -- wall cost 89 | return 100 90 | else 91 | -- normal step cost 92 | return 1 93 | end 94 | end 95 | 96 | local function reconstructPath(cameFrom, goal) 97 | local totalPath = {current} 98 | 99 | local current = cameFrom[goal] 100 | while current do 101 | table.insert(totalPath, current) 102 | current = cameFrom[current] 103 | end 104 | return totalPath 105 | end 106 | 107 | local function findPath() 108 | local frontier = pqueue() 109 | local cameFrom = {} 110 | local costSoFar = { 111 | [start] = 0, 112 | } 113 | frontier[start] = 0 114 | storeCell(start, goal) 115 | 116 | while not frontier.empty() do 117 | local current = assert((frontier.min())[1]) 118 | -- current == goal? 119 | if pEq(current, goal) then 120 | local path = reconstructPath(cameFrom, goal) 121 | coroutine.yield(true, path) 122 | return path 123 | end 124 | frontier.remove(current) 125 | 126 | for neighbour in neighbours(current) do 127 | local newCost = costSoFar[current] + cost(current, neighbour) 128 | 129 | if not costSoFar[neighbour] or (newCost < costSoFar[neighbour]) then 130 | --dprint(("[%d, %d] - [%d, %d] = %0.4f %0.4f %s"):format(current[1], current[2], neighbour[1], neighbour[2], newCost, costSoFar[neighbour] or -1, tostring(frontier[neighbour]))) 131 | costSoFar[neighbour] = newCost 132 | frontier[neighbour] = newCost + heuristicCostEstimate(goal, neighbour) 133 | cameFrom[neighbour] = current 134 | end 135 | end 136 | coroutine.yield(false, current) 137 | end 138 | coroutine.yield(true, {}) 139 | end 140 | 141 | xpcall(findPath, function(msg) 142 | print(msg) 143 | print(debug.traceback()) 144 | end) 145 | 146 | coroutine.yield(true, path) 147 | end) 148 | end 149 | 150 | 151 | solver.solve = solve 152 | return solver 153 | end -------------------------------------------------------------------------------- /serpent.lua: -------------------------------------------------------------------------------- 1 | local n, v = "serpent", 0.272 -- (C) 2012-13 Paul Kulchenko; MIT License 2 | local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" 3 | local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} 4 | local badtype = {thread = true, userdata = true, cdata = true} 5 | local keyword, globals, G = {}, {}, (_G or _ENV) 6 | for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 7 | 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 8 | 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end 9 | for k,v in pairs(G) do globals[v] = k end -- build func to name mapping 10 | for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do 11 | for k,v in pairs(G[g] or {}) do globals[v] = g..'.'..k end end 12 | 13 | local function s(t, opts) 14 | local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum 15 | local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge 16 | local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) 17 | local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) 18 | local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 19 | local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", 20 | -- tostring(val) is needed because __tostring may return a non-string value 21 | function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return syms[s] end)) end 22 | local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or s) 23 | or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 24 | or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end 25 | local function comment(s,l) return comm and (l or 0) < comm and ' --[['..tostring(s)..']]' or '' end 26 | local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal 27 | and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end 28 | local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] 29 | local n = name == nil and '' or name 30 | local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] 31 | local safe = plain and n or '['..safestr(n)..']' 32 | return (path or '')..(plain and path and '.' or '')..safe, safe end 33 | local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding 34 | local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} 35 | local function padnum(d) return ("%0"..maxn.."d"):format(d) end 36 | table.sort(k, function(a,b) 37 | -- sort numeric keys first: k[key] is not nil for numerical keys 38 | return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) 39 | < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end 40 | local function val2str(t, name, indent, insref, path, plainindex, level) 41 | local ttype, level, mt = type(t), (level or 0), getmetatable(t) 42 | local spath, sname = safename(path, name) 43 | local tag = plainindex and 44 | ((type(name) == "number") and '' or name..space..'='..space) or 45 | (name ~= nil and sname..space..'='..space or '') 46 | if seen[t] then -- already seen this element 47 | sref[#sref+1] = spath..space..'='..space..seen[t] 48 | return tag..'nil'..comment('ref', level) end 49 | if type(mt) == 'table' and (mt.__serialize or mt.__tostring) then -- knows how to serialize itself 50 | seen[t] = insref or spath 51 | if mt.__serialize then t = mt.__serialize(t) else t = tostring(t) end 52 | ttype = type(t) end -- new value falls through to be serialized 53 | if ttype == "table" then 54 | if level >= maxl then return tag..'{}'..comment('max', level) end 55 | seen[t] = insref or spath 56 | if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty 57 | local maxn, o, out = math.min(#t, maxnum or #t), {}, {} 58 | for key = 1, maxn do o[key] = key end 59 | if not maxnum or #o < maxnum then 60 | local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables 61 | for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end 62 | if maxnum and #o > maxnum then o[maxnum+1] = nil end 63 | if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end 64 | local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) 65 | for n, key in ipairs(o) do 66 | local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse 67 | if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing 68 | or opts.keyallow and not opts.keyallow[key] 69 | or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types 70 | or sparse and value == nil then -- skipping nils; do nothing 71 | elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then 72 | if not seen[key] and not globals[key] then 73 | sref[#sref+1] = 'placeholder' 74 | local sname = safename(iname, gensym(key)) -- iname is table for local variables 75 | sref[#sref] = val2str(key,sname,indent,sname,iname,true) end 76 | sref[#sref+1] = 'placeholder' 77 | local path = seen[t]..'['..(seen[key] or globals[key] or gensym(key))..']' 78 | sref[#sref] = path..space..'='..space..(seen[value] or val2str(value,nil,indent,path)) 79 | else 80 | out[#out+1] = val2str(value,key,indent,insref,seen[t],plainindex,level+1) 81 | end 82 | end 83 | local prefix = string.rep(indent or '', level) 84 | local head = indent and '{\n'..prefix..indent or '{' 85 | local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) 86 | local tail = indent and "\n"..prefix..'}' or '}' 87 | return (custom and custom(tag,head,body,tail) or tag..head..body..tail)..comment(t, level) 88 | elseif badtype[ttype] then 89 | seen[t] = insref or spath 90 | return tag..globerr(t, level) 91 | elseif ttype == 'function' then 92 | seen[t] = insref or spath 93 | local ok, res = pcall(string.dump, t) 94 | local func = ok and ((opts.nocode and "function() --[[..skipped..]] end" or 95 | "((loadstring or load)("..safestr(res)..",'@serialized'))")..comment(t, level)) 96 | return tag..(func or globerr(t, level)) 97 | else return tag..safestr(t) end -- handle all other types 98 | end 99 | local sepr = indent and "\n" or ";"..space 100 | local body = val2str(t, name, indent) -- this call also populates sref 101 | local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' 102 | local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' 103 | return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" 104 | end 105 | 106 | local function deserialize(data, opts) 107 | local f, res = (loadstring or load)('return '..data) 108 | if not f then f, res = (loadstring or load)(data) end 109 | if not f then return f, res end 110 | if opts and opts.safe == false then return pcall(f) end 111 | 112 | local count, thread = 0, coroutine.running() 113 | local h, m, c = debug.gethook(thread) 114 | debug.sethook(function (e, l) count = count + 1 115 | if count >= 3 then error("cannot call functions") end 116 | end, "c") 117 | local res = {pcall(f)} 118 | count = 0 -- set again, otherwise it's tripped on the next sethook 119 | debug.sethook(thread, h, m, c) 120 | return (table.unpack or unpack)(res) 121 | end 122 | 123 | local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end 124 | return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, 125 | load = deserialize, 126 | dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, 127 | line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, 128 | block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } 129 | --------------------------------------------------------------------------------