513 lines
11 KiB
Lua
513 lines
11 KiB
Lua
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 = "<x> <y> <z>",
|
|
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()
|