-- LUALOCALS < --------------------------------------------------------- local io, ipairs, math, minetest, pairs, string, table, type = io, ipairs, math, minetest, pairs, string, table, type local io_open, math_floor, string_gsub, string_match, string_rep, table_concat, table_sort = io.open, math.floor, string.gsub, string.match, string.rep, table.concat, table.sort -- LUALOCALS > --------------------------------------------------------- local modname = minetest.get_current_modname() minetest.register_privilege(modname, { description = "Can view mapblock usage stats", give_to_singleplayer = false, give_to_admin = true }) ------------------------------------------------------------------------ -- IN-MEMORY DATABASE AND UTILITY local db = {} local function getsub(tbl, id) local x = tbl[id] if x then return x end x = {} tbl[id] = x return x end local function statadd(blockid, playername, stat, value) local t = getsub(db, blockid) t = getsub(t, playername) t[stat] = (t[stat] or 0) + value end local function getpn(whom) if not whom then return end local pn = whom.get_player_name if not pn then return end pn = pn(whom) if not pn or not pn:find("%S") then return end return pn end local function blockid(pos) return math_floor((pos.x + 0.5) / 16) + 4096 * math_floor((pos.y + 0.5) / 16) + 16777216 * math_floor((pos.z + 0.5) / 16) end ------------------------------------------------------------------------ -- PLAYER ACTIVITY EVENT HOOKS local function reghook(func, stat, pwhom, ppos) return func(function(...) local t = {...} local whom = t[pwhom] local pn = getpn(whom) if not pn then return end local pos = ppos and t[ppos] or whom:get_pos() local id = blockid(pos) return statadd(id, pn, stat, 1) end) end reghook(minetest.register_on_dignode, "dig", 3, 1) reghook(minetest.register_on_placenode, "place", 3, 1) reghook(minetest.register_on_dieplayer, "die", 1) reghook(minetest.register_on_respawnplayer, "spawn", 1) reghook(minetest.register_on_joinplayer, "join", 1) reghook(minetest.register_on_leaveplayer, "leave", 1) reghook(minetest.register_on_craft, "craft", 2) minetest.register_on_player_hpchange(function(whom, change) local pn = getpn(whom) if not pn then return end local id = blockid(whom:get_pos()) if change < 0 then return statadd(id, pn, "hurt", -change) else return statadd(id, pn, "heal", change) end end) ------------------------------------------------------------------------ -- PLAYER MOVEMENT/IDLE HOOKS local playdb = {} local idlemin = 5 local function procstep(dt, player) local pn = getpn(player) if not pn then return end local pd = getsub(playdb, pn) local pos = player:get_pos() local dir = player:get_look_dir() local cur = {pos.x, pos.y, pos.z, dir.x, dir.y, dir.z} local moved if pd.last then for i = 1, 6 do moved = moved or pd.last[i] ~= cur[i] end end pd.last = cur local id = blockid(pos) local t = pd.t or 0 if moved then pd.t = 0 if t >= idlemin then statadd(id, pn, "idle", t) return statadd(id, pn, "move", dt) else return statadd(id, pn, "move", t + dt) end else if t >= idlemin then return statadd(id, pn, "idle", dt) else pd.t = t + dt if (t + dt) >= idlemin then return statadd(id, pn, "idle", t + dt) end end end end minetest.register_globalstep(function(dt) for _, player in pairs(minetest.get_connected_players()) do procstep(dt, player) end end) ------------------------------------------------------------------------ -- DATABASE FLUSH CYCLE local function deepadd(t, u) for k, v in pairs(u) do if type(v) == "table" then t[k] = deepadd(t[k] or {}, v) else t[k] = (t[k] or 0) + v end end return t end local function dbpath(id) local p = minetest.get_worldpath() .. "/" .. modname if id then id = "" .. id if id:sub(1, 3) ~= "blk" then id = "blk" .. id .. ".json" end p = p .. "/" .. id end return p end local dbload, dbsave do local genprev = {} local gennext = {} function dbload(id) local c = gennext[id] if c then return c end c = genprev[id] if c then gennext[id] = c return c end local f = io_open(dbpath(id)) if not f then return {} end local s = f:read("*all") f:close() s = s and minetest.parse_json(s) if s then gennext[id] = s end return s end function dbsave(id, data) gennext[id] = data minetest.safe_file_write(dbpath(id), minetest.write_json(data)) end local function gc() genprev = gennext gennext = {} minetest.after(60, gc) end gc() end local lasttime = minetest.get_us_time() / 1000000 local savedqty = 0 local alltime = 0 local runtime = 0 local function dbflush(forcerpt) local now = minetest.get_us_time() / 1000000 alltime = alltime + now - lasttime lasttime = now minetest.mkdir(dbpath()) for id, t in pairs(db) do t = deepadd(dbload(id), t) dbsave(id, t) savedqty = savedqty + 1 end db = {} now = minetest.get_us_time() / 1000000 runtime = runtime + now - lasttime if not forcerpt and ((runtime < 1 and alltime < 3600 and savedqty < 100) or savedqty < 1) then return end local function ms(i) return math_floor(i * 1000000) / 1000 end minetest.log("action", modname .. ": recorded " .. savedqty .. " block(s) using " .. ms(runtime) .. "ms out of " .. ms(alltime) .. "ms (" .. (math_floor(runtime / alltime * 10000) / 100) .. "%)") runtime = 0 alltime = 0 savedqty = 0 end local function flushcycle() dbflush() return minetest.after(60, flushcycle) end flushcycle() minetest.register_on_shutdown(function() for _, player in pairs(minetest.get_connected_players()) do local pn = getpn(player) if pn then local id = blockid(player:get_pos()) statadd(id, pn, "shutdown", 1) end end dbflush(true) end) ------------------------------------------------------------------------ -- STATS HUD local huds = {} local function hudcheck(player) local pname = player:get_player_name() local hud = huds[pname] local param = player:get_meta():get_string(modname) or "" if param ~= "" and not minetest.check_player_privs(pname, {[modname] = true}) then param = "" end if param == "" then if not hud then return end for _, v in ipairs(hud) do player:hud_remove(v.kid) player:hud_remove(v.vid) end huds[pname] = nil return end local id = blockid(player:get_pos()) local stats = deepadd(deepadd({}, getsub(db, id)), dbload(id)) param = param ~= "" and param or "place dig" local match = {} for _, v in pairs(param:split(" ")) do match[v] = true end local list = {} for k, v in pairs(stats) do local nv = 0 for ik, iv in pairs(v) do if match[ik] or param == "*" then nv = nv + iv end end if nv ~= 0 then list[#list + 1] = {k = k, v = nv} end end table_sort(list, function(a, b) return a.v > b.v end) local otherk = 0 local otherv = 0 while #list > 10 do otherk = otherk + 1 otherv = otherv + list[#list].v list[#list] = nil end if otherk ~= 0 then list[#list + 1] = { k = "(" .. otherk .. " others)", v = otherv } end for i = 1, #list do local p = list[i] local pref = string_rep("\n", i - 1) p.k = pref .. p.k p.v = pref .. math_floor(p.v) end if not hud then hud = {} huds[pname] = hud end for i = 1, #list do local p = list[i] local v = hud[i] if v then if v.kt ~= p.k then player:hud_change(v.kid, "text", p.k) v.kt = p.k end if v.vt ~= p.v then player:hud_change(v.vid, "text", p.v) v.vt = p.v end else hud[i] = { kt = p.k, vt = p.v, kid = player:hud_add({ hud_elem_type = "text", position = {x = 0.8, y = 0}, text = p.k, alignment = {x = 1, y = 1}, number = 0xC0C0C0, offset = {x = -4, y = 4} }), vid = player:hud_add({ hud_elem_type = "text", position = {x = 0.8, y = 0}, text = p.v, alignment = {x = -1, y = 1}, number = 0xC0C0C0, offset = {x = -8, y = 4} }), } end end for i = #hud, #list + 1, -1 do local v = hud[i] player:hud_remove(v.kid) player:hud_remove(v.vid) hud[i] = nil end end local function hudloop() minetest.after(0.25, hudloop) for _, player in pairs(minetest.get_connected_players()) do hudcheck(player) end end minetest.after(0, hudloop) minetest.register_chatcommand("blockstats", { description = "Toggle display of current mapblock stats", param = "[type [type [...]]] (* for all, blank to disable)", privs = {[modname] = true}, func = function(name, param) local player = minetest.get_player_by_name(name) if not player then return false, "must be online" end player:get_meta():set_string(modname, param) end }) ------------------------------------------------------------------------ -- REPORT COMMAND local function fmtrpt(t, id) local p = {} for k, v in pairs(t) do local n = 0 for _, v2 in pairs(v) do n = n + v2 end p[#p + 1] = k p[k] = n end table_sort(p, function(a, b) if p[a] == p[b] then return a < b end return p[a] > p[b] end) local r = id and {"block", id} or {"world"} for _, k in ipairs(p) do r[#r + 1] = "[" .. k .. "]" local v = t[k] local s = {} for k2 in pairs(v) do s[#s + 1] = k2 end table_sort(s, function(a, b) if v[a] == v[b] then return a < b end return v[a] > v[b] end) for _, k2 in ipairs(s) do r[#r + 1] = k2 r[#r + 1] = math_floor(v[k2]) end end return table_concat(r, " ") end minetest.register_chatcommand("blockuse", { privs = {server = true}, description = "Statistics about usage within the current mapblock.", func = function(name) local player = minetest.get_player_by_name(name) if not player then return false, "must be online" end local id = blockid(player:get_pos()) local t = deepadd(deepadd({}, getsub(db, id)), dbload(id)) minetest.chat_send_player(name, fmtrpt(t, id)) end }) ------------------------------------------------------------------------ -- DATABASE MIGRATION do local started = minetest.get_us_time() local clean = 0 local dirty = 0 local fail = 0 local dirlist = minetest.get_dir_list(dbpath(), false) local exists = {} for _, v in ipairs(dirlist) do exists[v] = true end for _, v in ipairs(dirlist) do if string_match(v, "^blk.*%.txt$") then local jv = string_gsub(v, "%.txt$", ".json") if exists[jv] then clean = clean + 1 else local f = io_open(dbpath(v)) if not f then fail = fail + 1 else local s = f:read("*all") f:close() local u = minetest.deserialize(s) if not u then fail = fail + 1 else dbsave(jv, u) dirty = dirty + 1 end end end end end local ms = math_floor((minetest.get_us_time() - started) / 1000) if clean + dirty + fail == 0 then minetest.log("action", modname .. ": db checked in " .. ms .. "ms") else minetest.log("action", modname .. ": db converted " .. dirty .. ", " .. fail .. " failed, " .. clean .. " skipped in " .. ms .. "ms") end end ------------------------------------------------------------------------