2019-12-06 19:07:56 -05:00

279 lines
6.6 KiB
Lua

-- LUALOCALS < ---------------------------------------------------------
local io, ipairs, math, minetest, pairs, table, type
= io, ipairs, math, minetest, pairs, table, type
local io_open, math_floor, table_concat, table_sort
= io.open, math.floor, table.concat, table.sort
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
------------------------------------------------------------------------
-- 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 .. ".txt"
end
p = p .. "/" .. id
end
return p
end
local function dbload(id)
local f = io_open(dbpath(id))
if not f then return {} end
local u = minetest.deserialize(f:read("*all"))
f:close()
return u
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)
minetest.safe_file_write(dbpath(id), minetest.serialize(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(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)
------------------------------------------------------------------------
-- CHAT 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 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
})
minetest.register_chatcommand("worlduse", {
privs = {server = true},
description = "Statistics about usage across the entire world.",
func = function(name)
local t = {}
for _, v in pairs(db) do
t = deepadd(t, v)
end
for _, v in ipairs(minetest.get_dir_list(dbpath(), false)) do
t = deepadd(t, dbload(v))
end
minetest.chat_send_player(name, fmtrpt(t))
end
})
------------------------------------------------------------------------