-- Localize globals local assert, error, io, loadfile, math, minetest, modlib, pairs, setfenv, setmetatable, type = assert, error, io, loadfile, math, minetest, modlib, pairs, setfenv, setmetatable, type -- Set environment local _ENV = {} setfenv(1, _ENV) -- Default value reference_strings = true -- Note: keys may not be marked as weak references: garbage collected log files wouldn't close the file: -- The `__gc` metamethod doesn't work for tables in Lua 5.1; a hack using `newproxy` would be needed -- See https://stackoverflow.com/questions/27426704/lua-5-1-workaround-for-gc-metamethod-for-tables) -- Therefore, :close() must be called on log files to remove them from the `files` table local files = {} local metatable = {__index = _ENV} _ENV.metatable = metatable function new(file_path, root, reference_strings) local self = setmetatable({ file_path = assert(file_path), root = root, reference_strings = reference_strings }, metatable) if minetest then files[self] = true end return self end local function set_references(self, table) -- Weak table keys to allow the collection of dead reference tables -- TODO garbage collect strings in the references table self.references = setmetatable(table, {__mode = "k"}) end function load(self) -- Bytecode is blocked by the engine local read = assert(loadfile(self.file_path)) -- math.huge is serialized to inf local env = {inf = math.huge} setfenv(read, env) read() env.R = env.R or {{}} local reference_count = #env.R for ref in pairs(env.R) do if ref > reference_count then -- Ensure reference count always has the value of the largest reference -- in case of "holes" (nil values) in the reference list reference_count = ref end end self.reference_count = reference_count self.root = env.R[1] set_references(self, {}) end function open(self) self.file = io.open(self.file_path, "a+") end function init(self) if modlib.file.exists(self.file_path) then self:load() self:_rewrite() self:open() return end self:open() self.root = {} self:_write() end function log(self, statement) self.file:write(statement) self.file:write"\n" end function flush(self) self.file:flush() end function close(self) self.file:close() self.file = nil files[self] = nil end if minetest then minetest.register_on_shutdown(function() for self in pairs(files) do self.file:close() end end) end local function _dump(self, value, is_key) if value == nil then return "nil" end if value == true then return "true" end if value == false then return "false" end if value ~= value then -- nan return "0/0" end local _type = type(value) if _type == "number" then return ("%.17g"):format(value) end local reference = self.references[value] if reference then return "R[" .. reference .."]" end reference = self.reference_count + 1 local key = "R[" .. reference .."]" local function create_reference() self.reference_count = reference self.references[value] = reference end if _type == "string" then local reference_strings = self.reference_strings if is_key and ((not reference_strings) or value:len() <= key:len()) and value:match"^[%a_][%a%d_]*$" then -- Short key return value, true end local formatted = ("%q"):format(value) if (not reference_strings) or formatted:len() <= key:len() then -- Short string return formatted end -- Use reference create_reference() self:log(key .. "=" .. formatted) elseif _type == "table" then -- Tables always need a reference before they are traversed to prevent infinite recursion create_reference() -- TODO traverse tables to determine whether this is actually needed self:log(key .. "={}") local tablelen = #value for k, v in pairs(value) do if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > tablelen then local dumped, short = _dump(self, k, true) self:log(key .. (short and ("." .. dumped) or ("[" .. dumped .. "]")) .. "=" .. _dump(self, v)) end end else error("unsupported type: " .. _type) end return key end function set(self, table, key, value) if not self.references[table] then error"orphan table" end if table[key] == value then -- No change return end table[key] = value table = _dump(self, table) local key, short_key = _dump(self, key, true) self:log(table .. (short_key and ("." .. key) or ("[" .. key .. "]")) .. "=" .. _dump(self, value)) end function set_root(self, key, value) return self:set(self.root, key, value) end function _write(self) set_references(self, {}) self.reference_count = 0 self:log"R={}" _dump(self, self.root) end function _rewrite(self) self.file = io.open(self.file_path, "w+") self:_write() self.file:close() end function rewrite(self) if self.file then self.file:close() end self:_rewrite() self:open() end -- Export environment return _ENV