2023-06-12 20:40:14 -04:00

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
------------------------------------------------------------------------