279 lines
6.6 KiB
Lua
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
|
|
})
|
|
|
|
------------------------------------------------------------------------
|