diff --git a/.gitignore b/.gitignore index 01ea94f..d002f61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ # generated by B3D test -character.b3d.lua \ No newline at end of file +character.b3d.lua +# generated by Lua logfile test +logfile.test.lua \ No newline at end of file diff --git a/Readme.md b/Readme.md index 53c1b62..4595a64 100644 --- a/Readme.md +++ b/Readme.md @@ -10,6 +10,23 @@ No dependencies. Licensed under the MIT License. Written by Lars Mueller aka LMD Mostly self-documenting code. Mod namespace is `modlib` or `_ml`, containing all variables & functions. +### Persistence + +#### Lua Log Files + +A data log file based on Lua statements. **Experimental.** High performance. Example from `test.lua`: + +```lua +local logfile = persistence.lua_log_file.new(mod.get_resource"logfile.test.lua", {}) +logfile:init() +logfile.root = {} +logfile:rewrite() +logfile:set_root({a = 1}, {b = 2, c = 3}) +logfile:close() +logfile:init() +assert(table.equals(logfile.root, {[{a = 1}] = {b = 2, c = 3}})) +``` + ### Bluon Binary Lua object notation. **Experimental.** Handling of subnormal numbers (very small floats) may be broken. diff --git a/init.lua b/init.lua index 9c7a564..45b68bc 100644 --- a/init.lua +++ b/init.lua @@ -50,7 +50,8 @@ for _, file in pairs{ "ranked_set", "binary", "b3d", - "bluon" + "bluon", + "persistence" } do modules[file] = file end @@ -77,7 +78,7 @@ end local load_module, get_resource, loadfile_exports modlib = setmetatable({ -- TODO bump on release - version = 62, + version = 63, modname = minetest and minetest.get_current_modname(), dir_delim = rawget(_G, "DIR_DELIM") or "/", _RG = setmetatable({}, { diff --git a/persistence.lua b/persistence.lua new file mode 100644 index 0000000..ebdd5d9 --- /dev/null +++ b/persistence.lua @@ -0,0 +1,140 @@ +lua_log_file = {} +local files = {} +local metatable = {__index = lua_log_file} + +-- TODO register on shutdown + +function lua_log_file.new(file_path, root) + local self = setmetatable({file_path = assert(file_path), root = root}, metatable) + files[self] = true + return self +end + +function lua_log_file:load() + -- Bytecode is blocked by the engine + local read = assert(loadfile(self.file_path)) + local env = {} + setfenv(read, env) + read() + env.R = env.R or {{}} + self.reference_count = #env.R + self.root = env.R[1] + self.references = modlib.table.flip(env.R) +end + +function lua_log_file:open() + self.file = io.open(self.file_path, "a+") +end + +function lua_log_file:init() + if modlib.file.exists(self.file_path) then + self:load() + self:_rewrite() + self:open() + return + end + self:open() + self.root = {} + self:_write() +end + +function lua_log_file:log(statement) + self.file:write(statement) + self.file:write"\n" +end + +function lua_log_file:flush() + self.file:flush() +end + +function lua_log_file:close() + self.file:close() + self.file = nil + files[self] = nil +end + +minetest.register_on_shutdown(function() + for self in pairs(files) do + self.file:close() + end +end) + +-- TODO use shorthand notations +function lua_log_file:dump(value) + 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 + if self.references[value] then + return "R[" .. self.references[value] .. "]" + end + self.reference_count = self.reference_count + 1 + local reference = self.reference_count + self.references[value] = reference + local formatted + if _type == "string" then + formatted = ("%q"):format(value) + elseif _type == "table" then + local entries = {} + for _, value in ipairs(value) do + table.insert(entries, self:dump(value)) + end + for key, value in pairs(value) do + if type(key) ~= "number" or key % 1 ~= 0 or key < 1 or key > #value then + table.insert(entries, "[" .. self:dump(key) .. "]=" .. self:dump(value)) + end + end + formatted = "{" .. table.concat(entries, ";") .. "}" + else + error("unsupported type: " .. _type) + end + local key = "R[" .. reference .."]" + self:log(key .. "=" .. formatted) + return key +end + +function lua_log_file:set(table, key, value) + table[key] = value + if not self.references[table] then + error"orphan table" + end + self:log(self:dump(table) .. "[" .. self:dump(key) .. "]=" .. self:dump(value)) +end + +function lua_log_file:set_root(key, value) + return self:set(self.root, key, value) +end + +function lua_log_file:_write() + self.references = {} + self.reference_count = 0 + self:log"R={}" + self:dump(self.root) +end + +function lua_log_file:_rewrite() + self.file = io.open(self.file_path, "w+") + self:_write() + self.file:close() +end + +function lua_log_file:rewrite() + if self.file then + self.file:close() + end + self:_rewrite() + self.file = io.open(self.file_path, "a+") +end \ No newline at end of file diff --git a/test.lua b/test.lua index 0531090..f158540 100644 --- a/test.lua +++ b/test.lua @@ -192,6 +192,15 @@ test_from_string("#333", 0x333333FF) test_from_string("#694269", 0x694269FF) test_from_string("#11223344", 0x11223344) +local logfile = persistence.lua_log_file.new(mod.get_resource"logfile.test.lua", {}) +logfile:init() +logfile.root = {} +logfile:rewrite() +logfile:set_root({a = 1}, {b = 2, c = 3}) +logfile:close() +logfile:init() +assert(table.equals(logfile.root, {[{a = 1}] = {b = 2, c = 3}})) + -- in-game tests & b3d testing local tests = { -- depends on player_api