Aaron Suen 0673f3094a Add options to disable YCTIWY
There's a "soft disable" that just hides the entities
but keeps the database up to date, and a "hard
disable" that clears the database, keeping only
minimal data, until existing transactions (taken
items, dumped inventories) are flushed to the
world.
2022-10-23 13:22:14 -04:00

440 lines
12 KiB
Lua

-- LUALOCALS < ---------------------------------------------------------
local ItemStack, math, minetest, nodecore, pairs, string, table, vector
= ItemStack, math, minetest, nodecore, pairs, string, table, vector
local math_pi, math_random, string_format, table_concat
= math.pi, math.random, string.format, table.concat
-- LUALOCALS > ---------------------------------------------------------
local disabled = nodecore.setting_bool(
"yctiwy_disable",
false,
"Disable offline player inventory database",
[[By default, players' offline position and inventory is saved
for displaying offline "ghost" entities, which can be hidden
by a different setting, but even when hidden the database is
still kept up to date. Enabling this setting will completely
disable that database and purge all data, saving server
resources, but making ghosts not display when reenabled until
each player has joined again at least once.]]
)
local hidden = nodecore.setting_bool(
"yctiwy_hide",
false,
"Do not display offline player entities",
[[By default, players' offline "ghosts" and inventories are
displayed as entities. Enabling this option hides
those, reducing resource impact on both client and server,
but also preventing players from accessing offline player
inventories. The offline database is still maintained, in
case the entities are later reenabled.]]
) or disabled
------------------------------------------------------------------------
-- DATABASE
local modname = minetest.get_current_modname()
local modstore = minetest.get_mod_storage()
local db = modstore:get_string("db")
if db == "" then db = nil end
db = db and minetest.deserialize(db)
db = db or {}
local drop = modstore:get_string("drop")
if drop == "" then drop = nil end
drop = drop and minetest.deserialize(drop)
drop = drop or {}
local function savedb()
modstore:set_string("db", minetest.serialize(db))
return modstore:set_string("drop", minetest.serialize(drop))
end
if disabled then
for k in pairs(db) do
if not db.taken then
db[k] = nil
end
end
savedb()
end
local function savestate(player)
if disabled or not nodecore.player_visible(player) then
db[player:get_player_name()] = nil
return
end
local ent = {
pos = player:get_pos(),
inv = {}
}
ent.pos.y = ent.pos.y + 1
local inv = player:get_inventory()
for i = 1, inv:get_size("main") do
local stack = inv:get_stack("main", i)
ent.inv[i] = stack:to_string()
end
ent.seen = minetest.get_gametime()
db[player:get_player_name()] = ent
end
------------------------------------------------------------------------
-- MISC/UTILITY/COMMON
local function areaunloaded(pos)
local basepos = vector.floor(pos)
if minetest.get_node(pos).name == "ignore" then return true end
for dx = -1, 1, 2 do
for dy = -1, 1, 2 do
for dz = -1, 1, 2 do
if minetest.get_node({
x = basepos.x + dx,
y = basepos.y + dy,
z = basepos.z + dz
}).name == "ignore" then return true end
end
end
end
end
local function box(n) return {-n, -n, -n, n, n, n} end
local desctitle = nodecore.translate("Offline Player")
local function getdesc(pname, stack)
local d = desctitle .. "\n" .. nodecore.notranslate(pname)
if stack then return d .. "\n" .. nodecore.touchtip_stack(stack) end
return d
end
local function spawnentity(pos, subname, values)
if areaunloaded(pos) then return end
local o = minetest.add_entity(pos, modname .. ":" .. subname)
o = o and o:get_luaentity()
if not o then return end
nodecore.log("info", modname .. ": spawned " .. subname .. " at "
.. minetest.pos_to_string(pos, 2) .. (values.pname
and (" for " .. values.pname) or ""))
for k, v in pairs(values) do o[k] = v end
return o:yctiwy_check()
end
local function entrystack(entry, slot)
if not entry then return end
if entry.taken and entry.taken[slot] then return end
local stack = entry.inv and entry.inv[slot]
if (not stack) or (stack == "") then return end
stack = ItemStack(stack)
local def = minetest.registered_items[stack:get_name()]
if (not def) or def.virtual_item then return end
return stack
end
------------------------------------------------------------------------
-- INVENTORY ENTITIES
local thiefdata = {}
minetest.register_entity(modname .. ":slotent", {
initial_properties = nodecore.stackentprops(),
is_yctiwy = true,
slot = 1,
loose = 0,
yctiwy_check = function(self)
local ent = db[self.pname]
if minetest.get_player_by_name(self.pname) then
return self.object:remove()
end
local stack = entrystack(ent, self.slot)
if not stack then return self.object:remove() end
local props = nodecore.stackentprops(stack)
props.visual_size.x = props.visual_size.x / 2
props.visual_size.y = props.visual_size.y / 2
if props.is_visible then
props.collisionbox = box(0.2)
end
if not self.yawed then
self.yawed = true
self.object:set_yaw(math_random() * math_pi * 2)
end
self.rotating = self.rotating or (math_random(1, 2) == 1) and 0.1 or -0.1
props.automatic_rotate = self.rotating
self.object:set_properties(props)
self.description = getdesc(self.pname, stack)
end,
on_punch = function(self, puncher)
local ent = db[self.pname]
local punchname = puncher and puncher:get_player_name()
if not punchname then return end
local state = thiefdata[punchname]
if state and (state.target ~= self.pname or state.slot ~= self.slot
or state.exp < nodecore.gametime) then state = nil end
state = state or {
target = self.pname,
slot = self.slot,
final = nodecore.gametime + 2
}
state.exp = nodecore.gametime + 2
thiefdata[punchname] = state
if nodecore.gametime < state.final then return end
local stack = entrystack(ent, self.slot)
if not stack then return end
local function steallog(desc)
nodecore.log("action", string_format(
"%s: %q %s %q slot %d item %q at %s",
modname, puncher:get_player_name(), desc,
self.pname, self.slot, stack:to_string(),
minetest.pos_to_string(ent.pos, 0)))
end
local rp = vector.round(ent.pos)
if minetest.is_protected(rp, punchname) and
not minetest.is_protected(rp, self.pname) then
steallog("attempts to steal")
minetest.record_protection_violation(rp, punchname)
return
end
steallog("steals")
ent.taken = ent.taken or {}
ent.taken[self.slot] = true
savedb()
stack = puncher:get_inventory():add_item("main", stack)
if not stack:is_empty() then
nodecore.item_eject(self.object:get_pos(), stack)
end
return self.object:remove()
end
})
local s = 0.25
local offsets = {
{x = -s, y = -s, z = -s},
{x = s, y = -s, z = -s},
{x = -s, y = s, z = -s},
{x = s, y = s, z = -s},
{x = -s, y = -s, z = s},
{x = s, y = s, z = s},
{x = -s, y = s, z = s},
{x = s, y = -s, z = s}
}
local function spawninv(name, entry, existdb)
entry = entry or db[name]
if not entry then return end
for i = 1, #offsets do
if not existdb[name .. ":" .. i] and entrystack(entry, i) then
local p = vector.add(offsets[i], entry.pos)
spawnentity(p, "slotent", {
pname = name,
slot = i
})
end
end
end
------------------------------------------------------------------------
-- MARKER ENTITY
local function markertexture(pname)
local colors = {nodecore.player_model_colors(pname)}
while #colors > 3 do colors[#colors] = nil end
for i = 1, #colors do
colors[i] = string_format("(%s_marker_%d.png^[multiply:%s)",
modname, i, colors[i])
end
return table_concat(colors, "^")
end
minetest.register_entity(modname .. ":marker", {
initial_properties = {
visual = "sprite",
textures = {"nc_player_wield_slot.png"},
collisionbox = box(0.1),
visual_size = {x = 0.2, y = 0.2},
is_visible = true,
static_save = false
},
is_yctiwy = true,
yctiwy_check = function(self)
if (not self.pname) or minetest.get_player_by_name(self.pname)
or (not minetest.player_exists(self.pname)) or (not db[self.pname]) then
return self.object:remove()
end
self.object:set_properties({textures = {markertexture(self.pname)}})
end,
on_punch = function(self)
self.object:set_properties({pointable = false})
self.pointtime = 2
self.on_step = function(me, dtime)
local pt = me.pointtime or 0
pt = pt - dtime
if pt > 0 then
me.pointtime = pt
else
me.object:set_properties({pointable = true})
me.pointtime = nil
me.on_step = nil
end
end
end
})
local function spawnmarker(name, entry, existdb)
if existdb[name .. ":"] then return end
entry = entry or db[name]
if not entry then return end
spawnentity(entry.pos, "marker", {
description = getdesc(name),
pname = name
})
end
------------------------------------------------------------------------
-- STOLEN ITEMS
local takenitem = modname .. ":taken"
nodecore.register_virtual_item(takenitem, {
description = "",
inventory_image = "[combine:1x1",
hotbar_type = "yctiwy_taken",
})
nodecore.register_aism({
itemnames = takenitem,
interval = 1,
chance = 1,
action = function(stack)
local exp = stack:get_meta():get_float("exp") or 0
if exp < nodecore.gametime then return "" end
end
})
------------------------------------------------------------------------
-- PLAYER HOOKS
minetest.register_on_leaveplayer(function(player)
savestate(player)
return savedb()
end)
minetest.register_on_shutdown(function()
for _, pl in pairs(minetest.get_connected_players()) do
savestate(pl)
end
return savedb()
end)
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
local ent = db[name]
if (not ent) or (not ent.taken) then return end
local inv = player:get_inventory()
local takestack = ItemStack(takenitem)
takestack:get_meta():set_float("exp", nodecore.gametime + 4)
for k in pairs(ent.taken) do
inv:set_stack("main", k, takestack)
end
for _, o in pairs(minetest.luaentities) do
if o and o.is_yctiwy and o.pname == name then
o.object:remove()
end
end
end)
------------------------------------------------------------------------
-- TIMER
local function dumpinv(ent)
if areaunloaded(ent.pos) then return end
for k, v in pairs(ent.inv) do
if not (ent.taken and ent.taken[k]) then
local stack = ItemStack(v)
if not stack:is_empty() then
if minetest.add_item(ent.pos, stack) then
minetest.log(string_format("%s: deleted player %q"
.. " drops slot %d item %q at %s",
modname, ent.pname, k, stack:to_string(), minetest.pos_to_string(ent.pos, 0)))
ent.inv[k] = ""
else
return
end
end
end
end
return true
end
local function dropplayer(name, ent, skipsave)
ent = ent or db[name]
ent.pname = name
drop[#drop + 1] = ent
db[name] = nil
return skipsave or savedb()
end
local oldremove = minetest.remove_player
function minetest.remove_player(name, ...)
local function helper(...)
dropplayer(name)
return ...
end
return helper(oldremove(name, ...))
end
local timer = 0
minetest.register_globalstep(function(dtime)
timer = timer - dtime
if timer > 0 then return end
timer = 3 + math_random() * 2
if not hidden then
for _, ent in pairs(minetest.luaentities) do
if ent.yctiwy_check then
ent:yctiwy_check()
end
end
end
local rollcall = {}
for _, pl in pairs(minetest.get_connected_players()) do
rollcall[pl:get_player_name()] = true
savestate(pl)
end
local existdb = {}
if not hidden then
for _, ent in pairs(minetest.luaentities) do
if ent.is_yctiwy then
existdb[(ent.pname or "") .. ":" .. (ent.slot or "")] = true
end
end
end
for name, ent in pairs(db) do
if not minetest.player_exists(name) then
dropplayer(name, ent, true)
elseif not (hidden or rollcall[name]) then
spawnmarker(name, ent, existdb)
spawninv(name, ent, existdb)
end
end
local batch = drop
drop = {}
for _, ent in pairs(batch) do
if areaunloaded(ent.pos) then
drop[#drop + 1] = ent
else
if not dumpinv(ent) then drop[#drop + 1] = ent end
end
end
return savedb()
end)