lots of mod info added
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
-- The Privileges Manager formspec: searchable player list on the left,
|
||||
-- toggle-switch privilege rows on the right. Built with formspec version 4
|
||||
-- (real coordinates) so it renders identically on Minetest Game and
|
||||
-- Mineclonia / VoxeLibre.
|
||||
|
||||
local FORMNAME = "priviledges_manager:main"
|
||||
local esc = minetest.formspec_escape
|
||||
|
||||
-- Per-viewer interface state. Cleared when the form is closed.
|
||||
local ctx = {}
|
||||
|
||||
local function get_ctx(name)
|
||||
ctx[name] = ctx[name] or { query = "", target = nil, filtered = {} }
|
||||
return ctx[name]
|
||||
end
|
||||
|
||||
-- Theme colours, adapted to the host game's palette.
|
||||
local THEME = priviledges_manager.is_mcl and {
|
||||
bg = "#0d0d0fee",
|
||||
panel = "#00000055",
|
||||
on = "#3fae54", -- switch ON (green)
|
||||
off = "#5a5a5e", -- switch OFF (grey)
|
||||
locked = "#3a2a2a", -- not grantable by this viewer
|
||||
} or {
|
||||
bg = "#23232bee",
|
||||
panel = "#0000004f",
|
||||
on = "#4caf50",
|
||||
off = "#6d6d75",
|
||||
locked = "#4a3030",
|
||||
}
|
||||
|
||||
-- Layout constants. The formspec is deliberately wide so that long
|
||||
-- privilege descriptions fit on the right without being clipped.
|
||||
local W, H = 19.0, 12.0
|
||||
local PRIV_ROW_H = 1.15
|
||||
local CONTENT_X, CONTENT_Y = 6.1, 2.2
|
||||
local CONTENT_W, CONTENT_H = 12.0, 9.0
|
||||
|
||||
-- Word-wrap `text` into at most `maxlines` lines of about `maxchars`
|
||||
-- characters each, appending an ellipsis if it had to be cut short. This is
|
||||
-- deterministic (no reliance on a widget auto-wrapping), so descriptions can
|
||||
-- never spill under the toggle button.
|
||||
local function wrap(text, maxchars, maxlines)
|
||||
local words = {}
|
||||
for w in tostring(text or ""):gmatch("%S+") do
|
||||
words[#words + 1] = w
|
||||
end
|
||||
|
||||
local lines, cur, i = {}, "", 1
|
||||
while i <= #words do
|
||||
local cand = cur == "" and words[i] or (cur .. " " .. words[i])
|
||||
if #cand <= maxchars then
|
||||
cur = cand
|
||||
i = i + 1
|
||||
elseif cur == "" then
|
||||
-- A single word longer than the line: hard-cut it.
|
||||
cur = words[i]:sub(1, maxchars - 1) .. "\u{2026}"
|
||||
i = i + 1
|
||||
else
|
||||
lines[#lines + 1] = cur
|
||||
cur = ""
|
||||
if #lines == maxlines then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if #lines < maxlines and cur ~= "" then
|
||||
lines[#lines + 1] = cur
|
||||
cur = ""
|
||||
end
|
||||
|
||||
-- Words still remaining means we ran out of lines: mark a truncation.
|
||||
if i <= #words then
|
||||
local last = lines[#lines] or ""
|
||||
if #last + 1 > maxchars then
|
||||
last = last:sub(1, maxchars - 1)
|
||||
end
|
||||
lines[#lines] = last .. "\u{2026}"
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
local function build_player_list(c)
|
||||
local players = priviledges_manager.players.list(c.query)
|
||||
c.filtered = players
|
||||
|
||||
local items, selidx = {}, 0
|
||||
for i, p in ipairs(players) do
|
||||
local marker = p.online and "\u{25CF} " or "\u{25CB} "
|
||||
local colour = p.online and "#7fe07f" or "#bfbfbf"
|
||||
items[i] = colour .. esc(marker .. p.name)
|
||||
if p.name == c.target then
|
||||
selidx = i
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(items, ","), selidx, #players
|
||||
end
|
||||
|
||||
local function build_priv_rows(c, viewer)
|
||||
if not c.target then
|
||||
return ("label[%f,%f;%s]"):format(
|
||||
CONTENT_X + 0.3, CONTENT_Y + 0.4,
|
||||
esc("Select a player on the left to manage their privileges."))
|
||||
end
|
||||
|
||||
local privs = priviledges_manager.privs.all()
|
||||
local held = minetest.get_player_privs(c.target)
|
||||
|
||||
-- Geometry shared by every row. The toggle is pinned to the right and the
|
||||
-- text is wrapped to end well before it, so they can never overlap.
|
||||
local btn_w, btn_h = 1.7, 0.7
|
||||
local btn_x = CONTENT_W - btn_w - 0.5 -- toggle pinned to the right
|
||||
local text_right = btn_x - 0.3 -- text must end before here
|
||||
local text_w = text_right - 0.5
|
||||
-- Conservative character budget for the wrapped description (tuned for the
|
||||
-- default font so it stays inside even if the smaller font is unavailable).
|
||||
local desc_chars = math.max(12, math.floor(text_w / 0.23))
|
||||
local LINE_H = 0.27
|
||||
|
||||
-- First pass: wrap every description (no truncation) and work out each
|
||||
-- row's height so the full text always fits. Rows grow as tall as needed.
|
||||
local rows = {}
|
||||
local y = 0.1
|
||||
for i, p in ipairs(privs) do
|
||||
local desc = (p.def and p.def.description) or ""
|
||||
local dlines = desc ~= "" and wrap(desc, desc_chars, math.huge) or {}
|
||||
local row_h = #dlines > 0
|
||||
and (0.52 + #dlines * LINE_H + 0.12) or 0.72
|
||||
rows[i] = { p = p, desc = desc, dlines = dlines, y = y, h = row_h }
|
||||
y = y + row_h + 0.06
|
||||
end
|
||||
local content_h = y + 0.05
|
||||
|
||||
-- Size the scrollbar to the actual content so the full list is reachable
|
||||
-- and the thumb reflects how much is visible.
|
||||
local scroll_factor = 0.1
|
||||
local overflow = math.max(0, content_h - CONTENT_H)
|
||||
local max_units = math.ceil(overflow / scroll_factor)
|
||||
local thumb = overflow > 0
|
||||
and math.max(1, math.floor(max_units * CONTENT_H / overflow)) or 0
|
||||
local value = math.min(c.scroll or 0, max_units)
|
||||
|
||||
local fs = {}
|
||||
fs[#fs + 1] = ("scrollbaroptions[arrows=default;max=%d;thumbsize=%d]"):format(
|
||||
max_units, thumb)
|
||||
fs[#fs + 1] = ("scrollbar[%f,%f;0.35,%f;vertical;privscroll;%d]"):format(
|
||||
CONTENT_X + CONTENT_W + 0.05, CONTENT_Y, CONTENT_H, value)
|
||||
fs[#fs + 1] = ("scroll_container[%f,%f;%f,%f;privscroll;vertical;%f]"):format(
|
||||
CONTENT_X, CONTENT_Y, CONTENT_W, CONTENT_H, scroll_factor)
|
||||
|
||||
-- Slightly smaller text for the privilege rows. Only affects labels emitted
|
||||
-- after this point (the rows + nothing else of note before the container end).
|
||||
fs[#fs + 1] = "style_type[label;font_size=*0.9]"
|
||||
|
||||
for _, r in ipairs(rows) do
|
||||
local p, desc, row_h = r.p, r.desc, r.h
|
||||
local is_on = held[p.name] == true
|
||||
local grantable = priviledges_manager.privs.can_grant(viewer, p.name)
|
||||
local btn = "t_" .. p.name
|
||||
|
||||
-- Row background.
|
||||
fs[#fs + 1] = ("box[0.1,%f;%f,%f;#ffffff10]"):format(
|
||||
r.y, CONTENT_W - 0.5, row_h)
|
||||
|
||||
-- Privilege name.
|
||||
fs[#fs + 1] = ("label[0.35,%f;%s]"):format(r.y + 0.27, esc(p.name))
|
||||
|
||||
-- Full description, wrapped across as many lines as it needs.
|
||||
for k, line in ipairs(r.dlines) do
|
||||
fs[#fs + 1] = ("label[0.5,%f;%s]"):format(
|
||||
r.y + 0.52 + (k - 1) * LINE_H, esc(line))
|
||||
end
|
||||
|
||||
-- The toggle switch itself: a coloured button, vertically centred.
|
||||
local colour = grantable and (is_on and THEME.on or THEME.off) or THEME.locked
|
||||
local face = is_on and "ON" or "OFF"
|
||||
if not grantable then
|
||||
face = "\u{1F512}" -- padlock: cannot change this privilege
|
||||
end
|
||||
fs[#fs + 1] = ("style[%s;bgcolor=%s;textcolor=#ffffff]"):format(btn, colour)
|
||||
fs[#fs + 1] = ("button[%f,%f;%f,%f;%s;%s]"):format(
|
||||
btn_x, r.y + (row_h - btn_h) / 2, btn_w, btn_h, btn, face)
|
||||
|
||||
-- Full description on hover: an area tooltip over the text, plus the
|
||||
-- same text on the toggle itself.
|
||||
local tip = desc ~= "" and desc or p.name
|
||||
if not grantable then
|
||||
tip = tip .. "\nYou are not allowed to change this privilege."
|
||||
end
|
||||
fs[#fs + 1] = ("tooltip[%f,%f;%f,%f;%s]"):format(
|
||||
0.1, r.y, text_right, row_h, esc(tip))
|
||||
fs[#fs + 1] = ("tooltip[%s;%s]"):format(btn, esc(tip))
|
||||
end
|
||||
|
||||
fs[#fs + 1] = "scroll_container_end[]"
|
||||
fs[#fs + 1] = "style_type[label;font_size=*1.0]"
|
||||
return table.concat(fs)
|
||||
end
|
||||
|
||||
local function build(viewer)
|
||||
local c = get_ctx(viewer)
|
||||
local list_items, selidx, count = build_player_list(c)
|
||||
|
||||
local target_label
|
||||
if c.target then
|
||||
local online = minetest.get_player_by_name(c.target) ~= nil
|
||||
local known = priviledges_manager.players.exists(c.target)
|
||||
target_label = "Managing: " .. c.target ..
|
||||
(online and " (online)" or known and " (offline)" or " (new account)")
|
||||
else
|
||||
target_label = "No player selected"
|
||||
end
|
||||
|
||||
local fs = {
|
||||
"formspec_version[4]",
|
||||
("size[%f,%f]"):format(W, H),
|
||||
("bgcolor[%s;true]"):format(THEME.bg),
|
||||
|
||||
-- Header.
|
||||
("label[0.4,0.55;%s]"):format(esc("\u{1F511} Privileges Manager \u{2014} " ..
|
||||
priviledges_manager.game_title)),
|
||||
|
||||
-- Left panel: search + player list.
|
||||
("box[0.3,1.0;5.5,%f;%s]"):format(H - 1.3, THEME.panel),
|
||||
"field_close_on_enter[query;false]",
|
||||
("field[0.5,1.25;3.7,0.7;query;;%s]"):format(esc(c.query)),
|
||||
"button[4.3,1.25;1.3,0.7;search;Search]",
|
||||
("tooltip[query;%s]"):format(esc(
|
||||
"Type part of a name and press Enter to filter the list.")),
|
||||
("textlist[0.5,2.15;5.1,%f;playerlist;%s;%d;false]"):format(
|
||||
H - 4.2, list_items, selidx),
|
||||
("label[0.5,%f;%s]"):format(H - 1.85,
|
||||
esc(count .. " player(s) \u{25CF} online \u{25CB} offline")),
|
||||
|
||||
-- Manage an arbitrary (possibly never-seen) account by exact name.
|
||||
"field_close_on_enter[newname;true]",
|
||||
("field[0.5,%f;3.7,0.7;newname;;]"):format(H - 1.5),
|
||||
("button[4.3,%f;1.3,0.7;manage;Open]"):format(H - 1.5),
|
||||
("tooltip[newname;%s]"):format(esc(
|
||||
"Manage an offline player by exact name, then press Enter / Open.")),
|
||||
|
||||
-- Right panel: privilege toggles.
|
||||
("box[6.0,1.0;%f,%f;%s]"):format(W - 6.3, H - 1.3, THEME.panel),
|
||||
("label[6.2,1.55;%s]"):format(esc(target_label)),
|
||||
build_priv_rows(c, viewer),
|
||||
|
||||
"button_exit[" .. ("%f,%f;2.0,0.7;close;Close]"):format(W - 2.3, H - 0.95),
|
||||
}
|
||||
|
||||
return table.concat(fs)
|
||||
end
|
||||
|
||||
function priviledges_manager.show(viewer, target)
|
||||
if target and target ~= "" then
|
||||
local c = get_ctx(viewer)
|
||||
c.target = target
|
||||
priviledges_manager.players.remember(target)
|
||||
end
|
||||
minetest.show_formspec(viewer, FORMNAME, build(viewer))
|
||||
end
|
||||
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= FORMNAME then
|
||||
return
|
||||
end
|
||||
local viewer = player:get_player_name()
|
||||
|
||||
-- Security: never trust the client. Re-check permission on every action.
|
||||
if not priviledges_manager.privs.can_use(viewer) then
|
||||
minetest.chat_send_player(viewer, "You may no longer use the Privileges Manager.")
|
||||
ctx[viewer] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
local c = get_ctx(viewer)
|
||||
|
||||
if fields.quit or fields.close then
|
||||
ctx[viewer] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
local rerender = false
|
||||
|
||||
-- Remember the scroll position so toggling a switch doesn't jump to top.
|
||||
if fields.privscroll then
|
||||
local ev = minetest.explode_scrollbar_event(fields.privscroll)
|
||||
if ev.type == "CHG" then
|
||||
c.scroll = ev.value
|
||||
end
|
||||
end
|
||||
|
||||
-- Live search (fires on Enter in the query field or the Search button).
|
||||
if fields.query ~= nil and fields.query ~= c.query then
|
||||
c.query = fields.query
|
||||
rerender = true
|
||||
end
|
||||
if fields.search then
|
||||
rerender = true
|
||||
end
|
||||
|
||||
-- Player picked from the list.
|
||||
if fields.playerlist then
|
||||
local ev = minetest.explode_textlist_event(fields.playerlist)
|
||||
if ev.type == "CHG" or ev.type == "DCL" then
|
||||
local item = c.filtered[ev.index]
|
||||
if item then
|
||||
c.target = item.name
|
||||
rerender = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Manage an arbitrary account by exact name.
|
||||
if (fields.manage or fields.key_enter_field == "newname")
|
||||
and fields.newname and fields.newname:trim() ~= "" then
|
||||
c.target = fields.newname:trim()
|
||||
priviledges_manager.players.remember(c.target)
|
||||
rerender = true
|
||||
end
|
||||
|
||||
-- A privilege toggle was pressed.
|
||||
for key in pairs(fields) do
|
||||
local priv = key:match("^t_(.+)$")
|
||||
if priv and c.target and minetest.registered_privileges[priv] then
|
||||
if priviledges_manager.privs.can_grant(viewer, priv) then
|
||||
local held = minetest.get_player_privs(c.target)
|
||||
priviledges_manager.privs.set(c.target, priv, not held[priv], viewer)
|
||||
else
|
||||
minetest.chat_send_player(viewer,
|
||||
"You are not allowed to change the '" .. priv .. "' privilege.")
|
||||
end
|
||||
rerender = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if rerender then
|
||||
priviledges_manager.show(viewer)
|
||||
end
|
||||
return true
|
||||
end)
|
||||
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
ctx[player:get_player_name()] = nil
|
||||
end)
|
||||
@@ -0,0 +1,67 @@
|
||||
-- Entry points: chat command, plus optional inventory buttons for the two
|
||||
-- common inventory frameworks (sfinv on Minetest Game, unified_inventory).
|
||||
|
||||
local function open_for(name, target)
|
||||
if not priviledges_manager.privs.can_use(name) then
|
||||
return false, "You need the 'privs' (or 'basic_privs') privilege to manage privileges."
|
||||
end
|
||||
priviledges_manager.show(name, target ~= "" and target or nil)
|
||||
return true
|
||||
end
|
||||
|
||||
minetest.register_chatcommand("privman", {
|
||||
params = "[player]",
|
||||
description = "Open the visual Privileges Manager (optionally for a player)",
|
||||
-- No priv requirement here so basic_privs holders can open it too;
|
||||
-- the real check happens in open_for and on every toggle.
|
||||
func = function(name, param)
|
||||
return open_for(name, param:trim())
|
||||
end,
|
||||
})
|
||||
|
||||
minetest.register_chatcommand("privileges_manager", {
|
||||
params = "[player]",
|
||||
description = "Alias for /privman",
|
||||
func = function(name, param)
|
||||
return open_for(name, param:trim())
|
||||
end,
|
||||
})
|
||||
|
||||
-- sfinv tab (Minetest Game and games that use sfinv).
|
||||
if minetest.get_modpath("sfinv") and sfinv and sfinv.register_page then
|
||||
sfinv.register_page("priviledges_manager:tab", {
|
||||
title = "Privileges",
|
||||
is_in_nav = function(self, player, context)
|
||||
return priviledges_manager.privs.can_use(player:get_player_name())
|
||||
end,
|
||||
get = function(self, player, context)
|
||||
return sfinv.make_formspec(player, context,
|
||||
"button[2.5,3;5,1;privman_open;Open Privileges Manager]"
|
||||
.. "label[0.5,1;Manage player privileges visually.]",
|
||||
false)
|
||||
end,
|
||||
on_player_receive_fields = function(self, player, context, fields)
|
||||
if fields.privman_open then
|
||||
priviledges_manager.show(player:get_player_name())
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- unified_inventory button (used by many games incl. some Mineclone setups).
|
||||
if minetest.get_modpath("unified_inventory") and unified_inventory
|
||||
and unified_inventory.register_button then
|
||||
unified_inventory.register_button("privman", {
|
||||
type = "image",
|
||||
image = "ui_skins_button.png", -- generic; falls back gracefully if missing
|
||||
tooltip = "Privileges Manager",
|
||||
action = function(player)
|
||||
if priviledges_manager.privs.can_use(player:get_player_name()) then
|
||||
priviledges_manager.show(player:get_player_name())
|
||||
end
|
||||
end,
|
||||
condition = function(player)
|
||||
return priviledges_manager.privs.can_use(player:get_player_name())
|
||||
end,
|
||||
})
|
||||
end
|
||||
@@ -0,0 +1,98 @@
|
||||
-- Player discovery: online players, plus every account the auth backend
|
||||
-- knows about (so offline players can be managed too).
|
||||
|
||||
priviledges_manager.players = {}
|
||||
|
||||
local storage = minetest.get_mod_storage()
|
||||
local STORE_KEY = "seen_players"
|
||||
|
||||
-- A fallback set of names we have personally seen join. This is only used
|
||||
-- when the active auth handler does not support iterate() (most custom
|
||||
-- auth handlers). The builtin handler does support it, covering everyone.
|
||||
local seen = minetest.deserialize(storage:get_string(STORE_KEY)) or {}
|
||||
|
||||
local function persist()
|
||||
storage:set_string(STORE_KEY, minetest.serialize(seen))
|
||||
end
|
||||
|
||||
local function remember(name)
|
||||
if name and name ~= "" and not seen[name] then
|
||||
seen[name] = true
|
||||
persist()
|
||||
end
|
||||
end
|
||||
priviledges_manager.players.remember = remember
|
||||
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
remember(player:get_player_name())
|
||||
end)
|
||||
|
||||
-- Returns a set { [name] = true } of every account we can discover.
|
||||
local function all_known_names()
|
||||
local names = {}
|
||||
|
||||
-- 1. Currently connected players.
|
||||
for _, player in ipairs(minetest.get_connected_players()) do
|
||||
names[player:get_player_name()] = true
|
||||
end
|
||||
|
||||
-- 2. Offline accounts via the auth handler, if it supports iteration.
|
||||
local handler = minetest.get_auth_handler and minetest.get_auth_handler()
|
||||
if handler and handler.iterate then
|
||||
-- iterate() returns the full pairs() triple (generator, state, control);
|
||||
-- capture all three so the for-loop has its iterator state.
|
||||
local ok, gen, state, control = pcall(handler.iterate)
|
||||
if ok and type(gen) == "function" then
|
||||
for name in gen, state, control do
|
||||
names[name] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 3. Anyone we have remembered ourselves (fallback / extra safety).
|
||||
for name in pairs(seen) do
|
||||
names[name] = true
|
||||
end
|
||||
|
||||
return names
|
||||
end
|
||||
|
||||
-- Returns a sorted array of { name = string, online = bool } filtered by
|
||||
-- a case-insensitive substring query. Online players are listed first.
|
||||
function priviledges_manager.players.list(query)
|
||||
query = (query or ""):lower()
|
||||
|
||||
local online = {}
|
||||
for _, player in ipairs(minetest.get_connected_players()) do
|
||||
online[player:get_player_name()] = true
|
||||
end
|
||||
|
||||
local result = {}
|
||||
for name in pairs(all_known_names()) do
|
||||
if query == "" or name:lower():find(query, 1, true) then
|
||||
result[#result + 1] = { name = name, online = online[name] == true }
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(result, function(a, b)
|
||||
if a.online ~= b.online then
|
||||
return a.online -- online players bubble to the top
|
||||
end
|
||||
return a.name:lower() < b.name:lower()
|
||||
end)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- True if the account exists in the auth database (used to warn when a
|
||||
-- never-seen name is opened, which would create a fresh account on grant).
|
||||
function priviledges_manager.players.exists(name)
|
||||
local handler = minetest.get_auth_handler and minetest.get_auth_handler()
|
||||
if handler and handler.get_auth then
|
||||
local ok, auth = pcall(handler.get_auth, name)
|
||||
if ok and auth then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return seen[name] == true or minetest.get_player_by_name(name) ~= nil
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
-- Privilege model: which privileges exist, who may grant them, and the
|
||||
-- safe online/offline grant + revoke operations.
|
||||
|
||||
priviledges_manager.privs = {}
|
||||
|
||||
-- Sorted array of { name = string, def = table } for every registered
|
||||
-- privilege. Works on any game because privileges are an engine concept.
|
||||
function priviledges_manager.privs.all()
|
||||
local list = {}
|
||||
for name, def in pairs(minetest.registered_privileges) do
|
||||
list[#list + 1] = { name = name, def = def }
|
||||
end
|
||||
table.sort(list, function(a, b)
|
||||
return a.name < b.name
|
||||
end)
|
||||
return list
|
||||
end
|
||||
|
||||
-- The set of privileges a "basic_privs" holder is allowed to manage.
|
||||
local function basic_set()
|
||||
local set = {}
|
||||
local conf = minetest.settings:get("basic_privs") or "interact,shout"
|
||||
for _, priv in ipairs(conf:split(",")) do
|
||||
set[priv:trim()] = true
|
||||
end
|
||||
return set
|
||||
end
|
||||
priviledges_manager.privs.basic_set = basic_set
|
||||
|
||||
-- May `granter` toggle privilege `priv`? Mirrors the engine's /grant rules:
|
||||
-- * "privs" -> may manage any privilege
|
||||
-- * "basic_privs" -> may manage only privileges in the basic_privs setting
|
||||
function priviledges_manager.privs.can_grant(granter, priv)
|
||||
local gp = minetest.get_player_privs(granter)
|
||||
if gp.privs then
|
||||
return true
|
||||
end
|
||||
if gp.basic_privs then
|
||||
return basic_set()[priv] == true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- May `name` open the manager at all? (Has any granting capability.)
|
||||
function priviledges_manager.privs.can_use(name)
|
||||
local gp = minetest.get_player_privs(name)
|
||||
return gp.privs == true or gp.basic_privs == true
|
||||
end
|
||||
|
||||
-- Apply a single privilege change. `value` true = grant, false = revoke.
|
||||
-- Works for offline players via the auth handler. set_player_privs runs the
|
||||
-- registered on_grant / on_revoke callbacks itself, so we must not double-run.
|
||||
function priviledges_manager.privs.set(target, priv, value, caller)
|
||||
local privs = minetest.get_player_privs(target)
|
||||
|
||||
-- Revoking must remove the key entirely; the engine warns on a `false`
|
||||
-- value passed through set_player_privs.
|
||||
privs[priv] = value and true or nil
|
||||
minetest.set_player_privs(target, privs)
|
||||
|
||||
-- Keep our own seen-list in sync so freshly created accounts show up.
|
||||
priviledges_manager.players.remember(target)
|
||||
|
||||
-- Notify the affected player if they are online.
|
||||
if minetest.get_player_by_name(target) then
|
||||
local verb = value and "granted to" or "revoked from"
|
||||
minetest.chat_send_player(target,
|
||||
minetest.colorize("#f5d76e",
|
||||
("[Privileges] '%s' was %s you by %s."):format(priv, verb, caller)))
|
||||
end
|
||||
|
||||
minetest.log("action",
|
||||
("[priviledges_manager] %s %s '%s' for %s"):format(
|
||||
caller, value and "granted" or "revoked", priv, target))
|
||||
end
|
||||
Reference in New Issue
Block a user