nm = {} nm.playerdb = {} nm.inspect_mode = {} nm.inspect_query = {} nm.write_cache = {} nm.write_cache_thresh = 20 nm.write_cache_timeout = 30 -- seconds local function try_load(libname) local success, lib success, lib = pcall(require, libname) if success then return lib else return nil end end local bit32, bit bit32 = try_load("bit32") bit = try_load("bit") -- Test whether libraries are usable for our purpose local bit32_big = true if bit32 then local t = 67 if bit32.lshift(t + 32768, 32) ~= 141025251164160 then bit32_big = false -- broken end end local bit_big = true if bit then local t = 67 if bit.lshift(t + 32768, 32) ~= 141025251164160 then bit_big = false -- broken end end local function write_u16(f, n) local s if bit32 then s = string.char(bit32.band(n, 255), bit32.rshift(n, 8)) elseif bit then s = string.char(bit.band(n, 255), bit.rshift(n, 8)) else s = string.char(n % 256, math.floor(n / (2 ^ 8))) end f:write(s) end local function read_u16(f) local s = f:read(2) if not s then return nil end local a, b = string.byte(s, 1, 2) if bit32 then return a + bit32.lshift(b, 8) elseif bit then return a + bit.lshift(b, 8) else return a + b * (2 ^ 8) end end local function write_s16(f, n) write_u16(f, n + 32768) end local function read_s16(f) local v = read_u16(f) if v then return v - 32768 else return nil end end local function hash_node_pos(x, y, z) if bit32 and bit32_big then return (x + 32768) + bit32.lshift(y + 32768, 16) + bit32.lshift(z + 32768, 32) elseif bit and bit_big then return (x + 32768) + bit.lshift(y + 32768, 16) + bit.lshift(z + 32768, 32) else return (x + 32768) + ((y + 32768) * (2 ^ 16)) + ((z + 32768) * (2 ^ 32)) end end local function player_db_load() local f = io.open(minetest.get_worldpath().."/nm_players.db", "r") if not f then nm.player_nid = 0 return end local id = 0 for ent in f:lines() do nm.playerdb[ent] = id id = id + 1 end nm.player_nid = id f:close() end local function player_db_add(name) local f = io.open(minetest.get_worldpath().."/nm_players.db", "a") if not f then minetest.log("error", "[NM] Failed to open player database") error() end f:write(name .. "\n") f:close() end local function player_db_lookup(name, no_new_entry) local id = nm.playerdb[name] if not id and not no_new_entry then id = nm.player_nid nm.playerdb[name] = id nm.player_nid = nm.player_nid + 1 player_db_add(name) end return id end local function player_db_lookup_pid(spid) local sname for name, pid in pairs(nm.playerdb) do if pid == spid then sname = name break end end return sname end local function nm_db_index() nm.overwrite_cache = {} local f = io.open(minetest.get_worldpath().."/nm.db", "rb") if not f then f = io.open(minetest.get_worldpath().."/nm.db", "wb") f:write("NMDB") -- char[4] magic value f:write(string.char(1)) -- u8 version f:close() nm.info = "0s, fresh database was created" return end local m = f:read(4) if m ~= "NMDB" then error("Wrong magic value '" .. m .. "', expected 'NMDB'") end local v = string.byte(f:read(1)) if v == 1 then -- ok else error("Unsupported version " .. v) end local seen = {} local i = 0 local x, y, z, ph local stime = os.time() local sclock = os.clock() while true do x = read_s16(f) -- s16 x if not x then break end z = read_s16(f) -- s16 z y = read_s16(f) -- s16 y f:read(2) -- u16 pid ph = hash_node_pos(x, y, z) if seen[ph] ~= nil then table.insert(nm.overwrite_cache, seen[ph]) end seen[ph] = i i = i + 1 end f:close() seen = nil local eclock = os.clock() local etime = os.time() nm.info = string.format("%ds(%.2fs CPU time), %d(%.2f%%) outdated entries, %d total entries", os.difftime(etime, stime), eclock - sclock, #nm.overwrite_cache, (#nm.overwrite_cache / i) * 100, i) minetest.log("action", "[NM] Read NM database in " .. nm.info) end local function nm_db_lookup(sx, sy, sz) local f = io.open(minetest.get_worldpath().."/nm.db", "rb") if not f then minetest.log("error", "[NM] Failed to open database") error() end local m = f:read(4) if m ~= "NMDB" then error("Wrong magic value '" .. m .. "', expected 'NMDB'") end local v = string.byte(f:read(1)) if v == 1 then -- ok else error("Unsupported version " .. v) end local x, y, z while true do x = read_s16(f) -- x if x == nil then break end if x ~= sx then f:read(6) -- z, y, pid else z = read_s16(f) -- z if z ~= sz then f:read(4) -- y, pid else y = read_s16(f) -- y if y ~= sy then f:read(2) -- pid else local pid = read_u16(f) f:close() return pid end end end end f:close() return nil end local function nm_db_lookup_multiple(poslist) local res = {} for i = 1, #poslist do res[i] = -1 end local f = io.open(minetest.get_worldpath().."/nm.db", "rb") if not f then minetest.log("error", "[NM] Failed to open database") error() end local m = f:read(4) if m ~= "NMDB" then error("Wrong magic value '" .. m .. "', expected 'NMDB'") end local v = string.byte(f:read(1)) if v == 1 then -- ok else error("Unsupported version " .. v) end local x, y, z, found while true do x = read_s16(f) -- x if x == nil then break end found = false for _, spos in ipairs(poslist) do if x == spos.x then found = true end end if not found then f:read(6) -- z, y, pid else z = read_s16(f) -- z found = false for _, spos in ipairs(poslist) do if x == spos.x and z == spos.z then found = true end end if not found then f:read(4) -- y, pid else y = read_s16(f) -- y found = 0 for i, spos in ipairs(poslist) do if x == spos.x and z == spos.z and y == spos.y then found = i end end if found == 0 then f:read(2) else res[found] = read_u16(f) end end end end f:close() return res end local function nm_db_add(x, y, z, pid, fidx) local f if fidx then f = io.open(minetest.get_worldpath().."/nm.db", "r+b") else f = io.open(minetest.get_worldpath().."/nm.db", "ab") end if not f then minetest.log("error", "[NM] Failed to open database") error() end if fidx then f:seek("set", 4 + 1 + (8 * fidx)) end write_s16(f, x) write_s16(f, z) write_s16(f, y) write_u16(f, pid) f:close() end local function fire_node_modify_raw(name, pos) local pid = player_db_lookup(name) if #nm.overwrite_cache > 0 then local fidx = table.remove(nm.overwrite_cache) nm_db_add(pos.x, pos.y, pos.z, pid, fidx) else nm_db_add(pos.x, pos.y, pos.z, pid) end end local function clean_write_cache() if #nm.write_cache <= 0 then return end for _, e in ipairs(nm.write_cache) do fire_node_modify_raw(e.name, e.pos) end nm.write_cache = {} end local write_cache_timer = 0 minetest.register_globalstep(function(dtime) write_cache_timer = write_cache_timer + dtime if write_cache_timer > nm.write_cache_timeout then clean_write_cache() write_cache_timer = 0 end end) local function fire_node_modify(name, pos) if nm.write_cache_thresh > 0 then table.insert(nm.write_cache, {name=name, pos=pos}) if #nm.write_cache > nm.write_cache_thresh then clean_write_cache() end else fire_node_modify_raw(name, pos) end end local function get_node_modify(pos) clean_write_cache() local pid = nm_db_lookup(pos.x, pos.y, pos.z) if pid == nil then return "" end local pname = player_db_lookup_pid(pid) if pname == nil then return -1 end return pname end local function get_node_modify_multiple(poslist) clean_write_cache() local pids = nm_db_lookup_multiple(poslist) local res = {} for _, pid in ipairs(pids) do if pid == -1 then table.insert(res, "") else local pname = player_db_lookup_pid(pid) if pname == nil then table.insert(res, -1) else table.insert(res, pname) end end end return res end local function get_node_ret2human(v, pos) if v == "" then return "No information available for " .. pos .. "." elseif v == -1 then return "Found an entry for " .. pos .. " but no matching player name mapping, the database might be corrupted." else return pos .. " was last touched by " .. v .. "." end end minetest.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack, pointed_thing) if not placer or not placer:is_player() then return end fire_node_modify(placer:get_player_name(), pos) end) minetest.register_on_dignode(function(pos, oldnode, digger) if not digger or not digger:is_player() then return end fire_node_modify(digger:get_player_name(), pos) end) minetest.register_chatcommand("nm", { params = " ", description = "Check who modified a node last", privs = {basic_privs=true}, func = function(name, param) local x, y, z = param:match('(.-) (.-) (.*)') x = tonumber(x) y = tonumber(y) z = tonumber(z) if x == nil or y == nil or z == nil then return false, "Invalid coordinates." end local pname = get_node_modify({x=x, y=y, z=z}) local ps = minetest.pos_to_string({x=x, y=y, z=z}) return true, get_node_ret2human(pname, ps) end, }) minetest.register_chatcommand("nm_inspect", { params = "", description = "Check who modified a node last (inspection mode)", privs = {basic_privs=true}, func = function(name, param) if nm.inspect_mode[name] then nm.inspect_mode[name] = false if #nm.inspect_query[name] == 0 then return true, "Inspection mode disabled." end minetest.chat_send_player(name, "Inspection mode disabled, executing search.") local res = get_node_modify_multiple(nm.inspect_query[name]) for i, pos in ipairs(nm.inspect_query[name]) do local pname = res[i] local ps = minetest.pos_to_string(pos) minetest.chat_send_player(name, get_node_ret2human(pname, ps)) end nm.inspect_query[name] = {} else nm.inspect_mode[name] = true nm.inspect_query[name] = {} return true, "Inspection mode enabled." end end, }) minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing) if not puncher:is_player() then return end local name = puncher:get_player_name() if nm.inspect_mode[name] then table.insert(nm.inspect_query[name], pointed_thing.under) end end) minetest.register_chatcommand("nm_info", { params = "", description = "Get information about NM database & configuration", privs = {basic_privs=true}, func = function(name, param) local a = string.format("Info: Database was read in %s, " .. "%d outdated entries queued for overwriting, %d unsaved changes", nm.info, #nm.overwrite_cache, #nm.write_cache) local cfg = a .. "\n" .. string.format("Configuration: Write cache size: %d, Write cache timeout: %ds, Libraries -> bit: ", nm.write_cache_thresh, nm.write_cache_timeout) if bit then cfg = cfg .. "available (big numbers: " if bit_big then cfg = cfg .. "working" else cfg = cfg .. "broken" end cfg = cfg .. "), " else cfg = cfg .. "unavailable, " end cfg = cfg .. "bit32: " if bit32 then cfg = cfg .. "available (big numbers: " if bit32_big then cfg = cfg .. "working" else cfg = cfg .. "broken" end cfg = cfg .. ")" else cfg = cfg .. "unavailable" end return true, cfg end, }) minetest.register_chatcommand("nm_reindex", { params = "", description = "Reindex the NM database", privs = {basic_privs=true}, func = function(name, param) clean_write_cache() nm_db_index() return true, "Read database in " .. nm.info end, }) player_db_load() nm_db_index()