485 lines
11 KiB
Lua
485 lines
11 KiB
Lua
-- LUALOCALS < ---------------------------------------------------------
|
|
local io, ipairs, math, minetest, pairs, string, table, tostring, type
|
|
= io, ipairs, math, minetest, pairs, string, table, tostring, 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)
|
|
|
|
local olddrop = minetest.item_drop
|
|
function minetest.item_drop(item, whom, pos, ...)
|
|
local pn = getpn(whom)
|
|
if pn then statadd(blockid(pos), pn, "drop", 1) end
|
|
return olddrop(item, whom, pos, ...)
|
|
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 dbload, dbsave
|
|
do
|
|
local modstore = minetest.get_mod_storage()
|
|
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 s = modstore:get_string(tostring(id))
|
|
if not (s and s ~= "") then return {} end
|
|
s = s and minetest.parse_json(s)
|
|
if s then gennext[id] = s end
|
|
return s
|
|
end
|
|
function dbsave(id, data)
|
|
gennext[id] = data
|
|
modstore:set_string(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
|
|
|
|
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("info", 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
|
|
|
|
if not hud then
|
|
hud = {}
|
|
huds[pname] = hud
|
|
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
|
|
|
|
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_on_leaveplayer(function(player)
|
|
huds[player:get_player_name()] = nil
|
|
end)
|
|
|
|
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 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 started = minetest.get_us_time()
|
|
|
|
local clean = 0
|
|
local dirty = 0
|
|
local fail = 0
|
|
|
|
local dirlist = minetest.get_dir_list(dbpath(), false)
|
|
local modstore = minetest.get_mod_storage()
|
|
local function migratefiles(ext, decode)
|
|
for _, v in ipairs(dirlist) do
|
|
if string_match(v, "^blk.*%." .. ext .. "$") then
|
|
local id = string_gsub(string_gsub(v, "^blk", ""),
|
|
"%." .. ext .. "$", "")
|
|
if modstore:get_string(id) == "" then
|
|
local f = io_open(dbpath(v))
|
|
if not f then
|
|
fail = fail + 1
|
|
else
|
|
local s = f:read("*all")
|
|
f:close()
|
|
local u = decode(s)
|
|
if not u then
|
|
fail = fail + 1
|
|
else
|
|
dbsave(id, u)
|
|
dirty = dirty + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
migratefiles("json", minetest.parse_json)
|
|
migratefiles("txt", minetest.deserialize)
|
|
local ms = math_floor((minetest.get_us_time() - started) / 1000)
|
|
if clean + dirty + fail == 0 then
|
|
minetest.log("info", modname .. ": db checked in " .. ms .. "ms")
|
|
else
|
|
minetest.log("info", modname .. ": db converted " .. dirty
|
|
.. ", " .. fail .. " failed, " .. clean .. " skipped in "
|
|
.. ms .. "ms")
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|