diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua index 1c94a58f..66f6c5d8 100644 --- a/builtin/common/misc_helpers.lua +++ b/builtin/common/misc_helpers.lua @@ -200,9 +200,6 @@ function table.indexof(list, val) return -1 end -assert(table.indexof({"foo", "bar"}, "foo") == 1) -assert(table.indexof({"foo", "bar"}, "baz") == -1) - -------------------------------------------------------------------------------- if INIT ~= "client" then function file_exists(filename) @@ -220,8 +217,6 @@ function string:trim() return (self:gsub("^%s*(.-)%s*$", "%1")) end -assert(string.trim("\n \t\tfoo bar\t ") == "foo bar") - -------------------------------------------------------------------------------- function math.hypot(x, y) local t @@ -245,6 +240,20 @@ function math.sign(x, tolerance) return 0 end +-------------------------------------------------------------------------------- +function math.factorial(x) + assert(x % 1 == 0 and x >= 0, "factorial expects a non-negative integer") + if x >= 171 then + -- 171! is greater than the biggest double, no need to calculate + return math.huge + end + local v = 1 + for k = 2, x do + v = v * k + end + return v +end + -------------------------------------------------------------------------------- function get_last_folder(text,count) local parts = text:split(DIR_DELIM) @@ -462,6 +471,12 @@ function core.explode_scrollbar_event(evt) return retval end +-------------------------------------------------------------------------------- +function core.rgba(r, g, b, a) + return a and string.format("#%02X%02X%02X%02X", r, g, b, a) or + string.format("#%02X%02X%02X", r, g, b) +end + -------------------------------------------------------------------------------- function core.pos_to_string(pos, decimal_places) local x = pos.x @@ -500,10 +515,6 @@ function core.string_to_pos(value) return nil end -assert(core.string_to_pos("10.0, 5, -2").x == 10) -assert(core.string_to_pos("( 10.0, 5, -2)").z == -2) -assert(core.string_to_pos("asd, 5, -2)") == nil) - -------------------------------------------------------------------------------- function core.string_to_area(value) local p1, p2 = unpack(value:split(") (")) @@ -545,6 +556,39 @@ function table.copy(t, seen) end return n end + + +function table.insert_all(t, other) + for i=1, #other do + t[#t + 1] = other[i] + end + return t +end + + +function table.key_value_swap(t) + local ti = {} + for k,v in pairs(t) do + ti[v] = k + end + return ti +end + + +function table.shuffle(t, from, to, random) + from = from or 1 + to = to or #t + random = random or math.random + local n = to - from + 1 + while n > 1 do + local r = from + n-1 + local l = from + random(0, n-1) + t[l], t[r] = t[r], t[l] + n = n-1 + end +end + + -------------------------------------------------------------------------------- -- mainmenu only functions -------------------------------------------------------------------------------- @@ -629,6 +673,13 @@ end -- Returns the exact coordinate of a pointed surface -------------------------------------------------------------------------------- function core.pointed_thing_to_face_pos(placer, pointed_thing) + -- Avoid crash in some situations when player is inside a node, causing + -- 'above' to equal 'under'. + if vector.equals(pointed_thing.above, pointed_thing.under) then + return pointed_thing.under + end + + local eye_height = placer:get_properties().eye_height local eye_offset_first = placer:get_eye_offset() local node_pos = pointed_thing.under local camera_pos = placer:get_pos() @@ -648,7 +699,7 @@ function core.pointed_thing_to_face_pos(placer, pointed_thing) end local fine_pos = {[nc] = node_pos[nc] + offset} - camera_pos.y = camera_pos.y + 1.625 + eye_offset_first.y / 10 + camera_pos.y = camera_pos.y + eye_height + eye_offset_first.y / 10 local f = (node_pos[nc] + offset - camera_pos[nc]) / look_dir[nc] for i = 1, #oc do @@ -656,3 +707,25 @@ function core.pointed_thing_to_face_pos(placer, pointed_thing) end return fine_pos end + +function core.string_to_privs(str, delim) + assert(type(str) == "string") + delim = delim or ',' + local privs = {} + for _, priv in pairs(string.split(str, delim)) do + privs[priv:trim()] = true + end + return privs +end + +function core.privs_to_string(privs, delim) + assert(type(privs) == "table") + delim = delim or ',' + local list = {} + for priv, bool in pairs(privs) do + if bool then + list[#list + 1] = priv + end + end + return table.concat(list, delim) +end diff --git a/builtin/game/auth.lua b/builtin/game/auth.lua index aa660b1f..47507eb5 100644 --- a/builtin/game/auth.lua +++ b/builtin/game/auth.lua @@ -82,18 +82,15 @@ end core.builtin_auth_handler = { get_auth = function(name) assert(type(name) == "string") - -- Figure out what password to use for a new player (singleplayer - -- always has an empty password, otherwise use default, which is - -- usually empty too) - local new_password_hash = "" - -- If not in authentication table, return nil - if not core.auth_table[name] then + local auth_entry = core.auth_table[name] + -- If no such auth found, return nil + if not auth_entry then return nil end -- Figure out what privileges the player should have. -- Take a copy of the privilege table local privileges = {} - for priv, _ in pairs(core.auth_table[name].privileges) do + for priv, _ in pairs(auth_entry.privileges) do privileges[priv] = true end -- If singleplayer, give all privileges except those marked as give_to_singleplayer = false @@ -106,15 +103,17 @@ core.builtin_auth_handler = { -- For the admin, give everything elseif name == core.settings:get("name") then for priv, def in pairs(core.registered_privileges) do - privileges[priv] = true + if def.give_to_admin then + privileges[priv] = true + end end end -- All done return { - password = core.auth_table[name].password, + password = auth_entry.password, privileges = privileges, -- Is set to nil if unknown - last_login = core.auth_table[name].last_login, + last_login = auth_entry.last_login, } end, create_auth = function(name, password) @@ -130,12 +129,13 @@ core.builtin_auth_handler = { set_password = function(name, password) assert(type(name) == "string") assert(type(password) == "string") - if not core.auth_table[name] then + local auth_entry = core.auth_table[name] + if not auth_entry then core.log("action", "[AUTH] Setting password for new player " .. name) core.builtin_auth_handler.create_auth(name, password) else core.log("action", "[AUTH] Setting password for existing player " .. name) - core.auth_table[name].password = password + auth_entry.password = password end return true end, @@ -143,12 +143,28 @@ core.builtin_auth_handler = { core.log("action", "[AUTH] Setting privileges for player " .. name) assert(type(name) == "string") assert(type(privileges) == "table") - if not core.auth_table[name] then - core.builtin_auth_handler.create_auth(name, + local auth_entry = core.auth_table[name] + if not auth_entry then + auth_entry = core.builtin_auth_handler.create_auth(name, core.get_password_hash(name, core.settings:get("default_password"))) end - core.auth_table[name].privileges = privileges + + -- Run grant callbacks + for priv, _ in pairs(privileges) do + if not auth_entry.privileges[priv] then + core.run_priv_callbacks(name, priv, nil, "grant") + end + end + + -- Run revoke callbacks + for priv, _ in pairs(auth_entry.privileges) do + if not privileges[priv] then + core.run_priv_callbacks(name, priv, nil, "revoke") + end + end + + auth_entry.privileges = privileges core.notify_authentication_modified(name) end, reload = function() @@ -163,10 +179,36 @@ core.builtin_auth_handler = { end, record_login = function(name) assert(type(name) == "string") - assert(core.auth_table[name]).last_login = os.time() + local auth_entry = core.auth_table[name] + assert(auth_entry) + auth_entry.last_login = os.time() end, } +core.register_on_prejoinplayer(function(name, ip) + if core.registered_auth_handler ~= nil then + return -- Don't do anything if custom auth handler registered + end + local auth_entry = core.auth_table + if auth_entry[name] ~= nil then + return + end + + local name_lower = name:lower() + for k in pairs(auth_entry) do + if k:lower() == name_lower then + return string.format("\nYou can not register as '%s'! ".. + "Another player called '%s' is already registered. ".. + "Please check the spelling if it's your account ".. + "or use a different name.", name, k) + end + end +end) + +-- +-- Authentication API +-- + function core.register_authentication_handler(handler) if core.registered_auth_handler then error("Add-on authentication handler already registered by "..core.registered_auth_handler_modname) @@ -198,7 +240,6 @@ core.auth_commit = auth_pass("commit") core.auth_reload() local record_login = auth_pass("record_login") - core.register_on_joinplayer(function(player) record_login(player:get_player_name()) end) @@ -207,23 +248,6 @@ core.register_on_shutdown(function() core.auth_commit() end) -core.register_on_prejoinplayer(function(name, ip) - local auth = core.auth_table - if auth[name] ~= nil then - return - end - - local name_lower = name:lower() - for k in pairs(auth) do - if k:lower() == name_lower then - return string.format("\nYou can not register as '%s'! ".. - "Another player called '%s' is already registered. ".. - "Please check the spelling if it's your account ".. - "or use a different name.", name, k) - end - end -end) - -- Autosave if not core.is_singleplayer() then local save_interval = 600 diff --git a/builtin/game/chatcommands.lua b/builtin/game/chat.lua similarity index 86% rename from builtin/game/chatcommands.lua rename to builtin/game/chat.lua index f46c73c1..9c12181d 100644 --- a/builtin/game/chatcommands.lua +++ b/builtin/game/chat.lua @@ -1,4 +1,4 @@ --- Minetest: builtin/game/chatcommands.lua +-- Minetest: builtin/game/chat.lua -- -- Chat command handler @@ -27,9 +27,9 @@ core.register_on_chat_message(function(name, message) local has_privs, missing_privs = core.check_player_privs(name, cmd_def.privs) if has_privs then core.set_last_run_mod(cmd_def.mod_origin) - local success, message = cmd_def.func(name, param) - if message then - core.chat_send_player(name, message) + local _, result = cmd_def.func(name, param) + if result then + core.chat_send_player(name, result) end else core.chat_send_player(name, "You don't have permission" @@ -41,7 +41,7 @@ end) if core.settings:get_bool("profiler.load") then -- Run after register_chatcommand and its register_on_chat_message - -- Before any chattcommands that should be profiled + -- Before any chatcommands that should be profiled profiler.init_chatcommand() end @@ -80,7 +80,7 @@ core.register_chatcommand_alias = register_chatcommand_alias -- core.register_chatcommand("me", { params = "", - description = "Display chat action (e.g., '/me orders a pizza' displays" + description = "Show chat action (e.g., '/me orders a pizza' displays" .. " ' orders a pizza')", privs = {shout = true}, func = function(name, param) @@ -93,7 +93,7 @@ core.register_chatcommand("admin", { func = function(name) local admin = core.settings:get("name") if admin then - return true, "The administrator of this server is "..admin.."." + return true, "The administrator of this server is " .. admin .. "." else return false, "There's no administrator named in the config file." end @@ -102,16 +102,44 @@ core.register_chatcommand("admin", { core.register_chatcommand("privs", { params = "[]", - description = "Print privileges of player", + description = "Show privileges of yourself or another player", func = function(caller, param) param = param:trim() local name = (param ~= "" and param or caller) + if not core.player_exists(name) then + return false, "Player " .. name .. " does not exist." + end return true, "Privileges of " .. name .. ": " .. core.privs_to_string( core.get_player_privs(name), ", ") end }) +core.register_chatcommand("haspriv", { + params = "", + description = "Return list of all online players with privilege.", + privs = {basic_privs = true}, + func = function(caller, param) + param = param:trim() + if param == "" then + return false, "Invalid parameters (see /help haspriv)" + end + if not core.registered_privileges[param] then + return false, "Unknown privilege!" + end + local privs = core.string_to_privs(param) + local players_with_priv = {} + for _, player in pairs(core.get_connected_players()) do + local player_name = player:get_player_name() + if core.check_player_privs(player_name, privs) then + table.insert(players_with_priv, player_name) + end + end + return true, "Players online with the \"" .. param .. "\" privilege: " .. + table.concat(players_with_priv, ", ") + end +}) + local function handle_grant_command(caller, grantname, grantprivstr) local caller_privs = core.get_player_privs(caller) if not (caller_privs.privs or caller_privs.basic_privs) then @@ -141,6 +169,10 @@ local function handle_grant_command(caller, grantname, grantprivstr) if privs_unknown ~= "" then return false, privs_unknown end + for priv, _ in pairs(grantprivs) do + -- call the on_grant callbacks + core.run_priv_callbacks(grantname, priv, caller, "grant") + end core.set_player_privs(grantname, privs) core.log("action", caller..' granted ('..core.privs_to_string(grantprivs, ', ')..') privileges to '..grantname) if grantname ~= caller then @@ -166,7 +198,7 @@ core.register_chatcommand("grant", { }) core.register_chatcommand("grantme", { - params = "|all", + params = " | all", description = "Grant privileges to yourself", func = function(name, param) if param == "" then @@ -178,7 +210,7 @@ core.register_chatcommand("grantme", { core.register_chatcommand("revoke", { params = " ( | all)", - description = "Remove privilege from player", + description = "Remove privileges from player", privs = {}, func = function(name, param) if not core.check_player_privs(name, {privs = true}) and @@ -202,12 +234,19 @@ core.register_chatcommand("revoke", { end end if revoke_priv_str == "all" then + revoke_privs = privs privs = {} else for priv, _ in pairs(revoke_privs) do privs[priv] = nil end end + + for priv, _ in pairs(revoke_privs) do + -- call the on_revoke callbacks + core.run_priv_callbacks(revoke_name, priv, name, "revoke") + end + core.set_player_privs(revoke_name, privs) core.log("action", name..' revoked (' ..core.privs_to_string(revoke_privs, ', ') @@ -233,11 +272,12 @@ core.register_chatcommand("setpassword", { toname = param:match("^([^ ]+) *$") raw_password = nil end + if not toname then return false, "Name field required" end - local act_str_past = "?" - local act_str_pres = "?" + + local act_str_past, act_str_pres if not raw_password then core.set_player_password(toname, "") act_str_past = "cleared" @@ -249,13 +289,14 @@ core.register_chatcommand("setpassword", { act_str_past = "set" act_str_pres = "sets" end + if toname ~= name then core.chat_send_player(toname, "Your password was " .. act_str_past .. " by " .. name) end - core.log("action", name .. " " .. act_str_pres - .. " password of " .. toname .. ".") + core.log("action", name .. " " .. act_str_pres .. + " password of " .. toname .. ".") return true, "Password of player \"" .. toname .. "\" " .. act_str_past end @@ -263,7 +304,7 @@ core.register_chatcommand("setpassword", { core.register_chatcommand("clearpassword", { params = "", - description = "Set empty password", + description = "Set empty password for a player", privs = {password = true}, func = function(name, param) local toname = param @@ -289,7 +330,7 @@ core.register_chatcommand("auth_reload", { core.register_chatcommand("remove_player", { params = "", - description = "Remove player data", + description = "Remove a player's data", privs = {server = true}, func = function(name, param) local toname = param @@ -323,7 +364,7 @@ core.register_chatcommand("auth_save", { core.register_chatcommand("teleport", { params = ",, | | ( ,,) | ( )", - description = "Teleport to player or position", + description = "Teleport to position or player", privs = {teleport = true}, func = function(name, param) -- Returns (pos, true) if found, otherwise (pos, false) @@ -347,7 +388,6 @@ core.register_chatcommand("teleport", { return pos, false end - local teleportee local p = {} p.x, p.y, p.z = string.match(param, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$") p.x = tonumber(p.x) @@ -358,37 +398,37 @@ core.register_chatcommand("teleport", { if p.x < -lm or p.x > lm or p.y < -lm or p.y > lm or p.z < -lm or p.z > lm then return false, "Cannot teleport out of map bounds!" end - teleportee = core.get_player_by_name(name) + local teleportee = core.get_player_by_name(name) if teleportee then teleportee:set_pos(p) - return true, "Teleporting to " .. core.pos_to_string(vector.round(p)) + return true, "Teleporting to " .. core.pos_to_string(p, 1) end end - local teleportee - local p - local target_name - target_name = param:match("^([^ ]+)$") - teleportee = core.get_player_by_name(name) + local target_name = param:match("^([^ ]+)$") + local teleportee = core.get_player_by_name(name) + + p = nil if target_name then local target = core.get_player_by_name(target_name) if target then p = target:get_pos() end end + if teleportee and p then p = find_free_position_near(p) teleportee:set_pos(p) return true, "Teleporting to " .. target_name - .. " at " .. core.pos_to_string(vector.round(p)) + .. " at " .. core.pos_to_string(p, 1) end if not core.check_player_privs(name, {bring = true}) then return false, "You don't have permission to teleport other players (missing bring privilege)" end - local teleportee - local p = {} + teleportee = nil + p = {} local teleportee_name teleportee_name, p.x, p.y, p.z = param:match( "^([^ ]+) +([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$") @@ -399,13 +439,11 @@ core.register_chatcommand("teleport", { if teleportee and p.x and p.y and p.z then teleportee:set_pos(p) return true, "Teleporting " .. teleportee_name - .. " to " .. core.pos_to_string(vector.round(p)) + .. " to " .. core.pos_to_string(p, 1) end - local teleportee - local p - local teleportee_name - local target_name + teleportee = nil + p = nil teleportee_name, target_name = string.match(param, "^([^ ]+) +([^ ]+)$") if teleportee_name then teleportee = core.get_player_by_name(teleportee_name) @@ -421,7 +459,7 @@ core.register_chatcommand("teleport", { teleportee:set_pos(p) return true, "Teleporting " .. teleportee_name .. " to " .. target_name - .. " at " .. core.pos_to_string(vector.round(p)) + .. " at " .. core.pos_to_string(p, 1) end return false, 'Invalid parameters ("' .. param @@ -440,7 +478,8 @@ core.register_chatcommand("set", { core.settings:set(setname, setvalue) return true, setname .. " = " .. setvalue end - local setname, setvalue = string.match(param, "([^ ]+) (.+)") + + setname, setvalue = string.match(param, "([^ ]+) (.+)") if setname and setvalue then if not core.settings:get(setname) then return false, "Failed. Use '/set -n ' to create a new setting." @@ -448,14 +487,16 @@ core.register_chatcommand("set", { core.settings:set(setname, setvalue) return true, setname .. " = " .. setvalue end - local setname = string.match(param, "([^ ]+)") + + setname = string.match(param, "([^ ]+)") if setname then - local setvalue = core.settings:get(setname) + setvalue = core.settings:get(setname) if not setvalue then setvalue = "" end return true, setname .. " = " .. setvalue end + return false, "Invalid parameters (see /help set)." end }) @@ -563,15 +604,12 @@ local function handle_give_command(cmd, giver, receiver, stackstring) core.log("action", giver .. " invoked " .. cmd .. ', stackstring="' .. stackstring .. '"') local ritems = core.registered_items - local rnodes = core.registered_nodes - if not string.match(stackstring, ":") and - not ritems[stackstring] and not rnodes[stackstring] then + if not string.match(stackstring, ":") and not ritems[stackstring] then local modslist = core.get_modnames() - local namestring = stackstring:match("(%w+)") table.insert(modslist, 1, "default") for _, modname in pairs(modslist) do - local namecheck = modname .. ":" .. namestring - if ritems[namecheck] or rnodes[namecheck] then + local namecheck = modname .. ":" .. stackstring:match("(%w+)") + if ritems[namecheck] then stackstring = modname .. ":" .. stackstring break end @@ -580,8 +618,11 @@ local function handle_give_command(cmd, giver, receiver, stackstring) local itemstack = ItemStack(stackstring) if itemstack:is_empty() then return false, "Cannot give an empty item" - elseif not itemstack:is_known() then + elseif (not itemstack:is_known()) or (itemstack:get_name() == "unknown") then return false, "Cannot give an unknown item" + -- Forbid giving 'ignore' due to unwanted side effects + elseif itemstack:get_name() == "ignore" then + return false, "Giving 'ignore' is not allowed" end local receiverref = core.get_player_by_name(receiver) if receiverref == nil then @@ -600,18 +641,18 @@ local function handle_give_command(cmd, giver, receiver, stackstring) -- entered (e.g. big numbers are always interpreted as 2^16-1). stackstring = itemstack:to_string() if giver == receiver then - return true, ("%q %sadded to inventory.") - :format(stackstring, partiality) + local msg = "%q %sadded to inventory." + return true, msg:format(stackstring, partiality) else core.chat_send_player(receiver, ("%q %sadded to inventory.") :format(stackstring, partiality)) - return true, ("%q %sadded to %s's inventory.") - :format(stackstring, partiality, receiver) + local msg = "%q %sadded to %s's inventory." + return true, msg:format(stackstring, partiality, receiver) end end core.register_chatcommand("give", { - params = " ", + params = " [ []]", description = "Give item to player", privs = {give = true}, func = function(name, param) @@ -624,7 +665,7 @@ core.register_chatcommand("give", { }) core.register_chatcommand("giveme", { - params = "", + params = " [ []]", description = "Give item to yourself", privs = {give = true}, func = function(name, param) @@ -652,6 +693,9 @@ core.register_chatcommand("spawnentity", { core.log("error", "Unable to spawn entity, player is nil") return false, "Unable to spawn entity, player is nil" end + if not core.registered_entities[entityname] then + return false, "Cannot spawn an unknown entity" + end if p == "" then p = player:get_pos() else @@ -686,8 +730,8 @@ core.register_chatcommand("pulverize", { core.rollback_punch_callbacks = {} core.register_on_punchnode(function(pos, node, puncher) - local name = puncher:get_player_name() - if core.rollback_punch_callbacks[name] then + local name = puncher and puncher:get_player_name() + if name and core.rollback_punch_callbacks[name] then core.rollback_punch_callbacks[name](pos, node, puncher) core.rollback_punch_callbacks[name] = nil end @@ -785,16 +829,20 @@ core.register_chatcommand("rollback", { }) core.register_chatcommand("status", { - description = "Print server status", + description = "Show server status", privs = {server = true}, func = function(name, param) - return true, core.get_server_status() + local status = core.get_server_status(name, false) + if status and status ~= "" then + return true, status + end + return false, "This command was disabled by a mod or game" end }) -core.register_chatcommand("settime", { - params = "<0..23>:<0..59> | <0..24000>", - description = "Set time of day", +core.register_chatcommand("time", { + params = "[<0..23>:<0..59> | <0..24000>]", + description = "Show or set time of day", privs = {}, func = function(name, param) if param == "" then @@ -831,10 +879,10 @@ core.register_chatcommand("settime", { return true, "Time of day changed." end }) -register_chatcommand_alias("time", "settime") +register_chatcommand_alias("settime", "time") core.register_chatcommand("days", { - description = "Display day count", + description = "Show day count since world creation", func = function(name, param) return true, "Current day is " .. core.get_day_count() end @@ -845,13 +893,15 @@ core.register_chatcommand("shutdown", { description = "Shutdown server (-1 cancels a delayed shutdown)", privs = {server = true}, func = function(name, param) - local delay, reconnect, message = param:match("([^ ][-]?[0-9]+)([^ ]+)(.*)") - message = message or "" + local delay, reconnect, message + delay, param = param:match("^%s*(%S+)(.*)") + if param then + reconnect, param = param:match("^%s*(%S+)(.*)") + end + message = param and param:match("^%s*(.+)") or "" + delay = tonumber(delay) or 0 - if delay ~= "" then - delay = tonumber(param) or 0 - else - delay = 0 + if delay == 0 then core.log("action", name .. " shuts down server") core.chat_send_all("*** Server shutting down (operator request).") end @@ -860,15 +910,20 @@ core.register_chatcommand("shutdown", { }) core.register_chatcommand("ban", { - params = "", - description = "Ban IP of player", + params = "[]", + description = "Ban the IP of a player or show the ban list", privs = {ban = true}, func = function(name, param) if param == "" then - return true, "Ban list: " .. core.get_ban_list() + local ban_list = core.get_ban_list() + if ban_list == "" then + return true, "The ban list is empty." + else + return true, "Ban list: " .. ban_list + end end if not core.get_player_by_name(param) then - return false, "No such player." + return false, "Player is not online." end if not core.ban_player(param) then return false, "Failed to ban player." @@ -881,7 +936,7 @@ core.register_chatcommand("ban", { core.register_chatcommand("unban", { params = " | ", - description = "Remove IP ban", + description = "Remove IP ban belonging to a player/IP", privs = {ban = true}, func = function(name, param) if not core.unban_player_or_ip(param) then @@ -912,7 +967,7 @@ core.register_chatcommand("kick", { }) core.register_chatcommand("clearobjects", { - params = "[full|quick]", + params = "[full | quick]", description = "Clear all objects in world", privs = {server = true}, func = function(name, param) @@ -957,10 +1012,11 @@ core.register_chatcommand("msg", { end }) register_chatcommand_alias("m", "msg") +register_chatcommand_alias("pm", "msg") core.register_chatcommand("last-login", { params = "[]", - description = "Get the last login time of a player", + description = "Get the last login time of a player or yourself", func = function(name, param) if param == "" then param = name @@ -983,7 +1039,7 @@ core.register_chatcommand("clearinv", { if param and param ~= "" and param ~= name then if not core.check_player_privs(name, {server = true}) then return false, "You don't have permission" - .. " to run this command (missing privilege: server)" + .. " to clear another player's inventory (missing privilege: server)" end player = core.get_player_by_name(param) core.chat_send_player(param, name.." cleared your inventory.") @@ -1059,13 +1115,9 @@ core.register_chatcommand("setspawn", { if not player then return false end - local pos = vector.round(player:get_pos()) - local x = pos.x - local y = pos.y - local z = pos.z - local pos_to_string = x .. "," .. y .. "," .. z - core.settings:set("static_spawnpoint", pos_to_string) - return true, "Setting spawn point to (" .. pos_to_string .. ")" + local pos = minetest.pos_to_string(player:get_pos(), 1) + core.settings:set("static_spawnpoint", pos) + return true, "The spawn point are set to (" .. pos .. ")" end }) diff --git a/builtin/game/constants.lua b/builtin/game/constants.lua index 50c515b2..0ee2a723 100644 --- a/builtin/game/constants.lua +++ b/builtin/game/constants.lua @@ -21,6 +21,10 @@ core.EMERGE_GENERATED = 4 -- constants.h -- Size of mapblocks in nodes core.MAP_BLOCKSIZE = 16 +-- Default maximal HP of a player +core.PLAYER_MAX_HP_DEFAULT = 20 +-- Default maximal breath of a player +core.PLAYER_MAX_BREATH_DEFAULT = 11 -- light.h -- Maximum value for node 'light_source' parameter diff --git a/builtin/game/detached_inventory.lua b/builtin/game/detached_inventory.lua index 1d4f0901..2e27168a 100644 --- a/builtin/game/detached_inventory.lua +++ b/builtin/game/detached_inventory.lua @@ -17,3 +17,8 @@ function core.create_detached_inventory(name, callbacks, player_name) core.detached_inventories[name] = stuff return core.create_detached_inventory_raw(name, player_name) end + +function core.remove_detached_inventory(name) + core.detached_inventories[name] = nil + return core.remove_detached_inventory_raw(name) +end diff --git a/builtin/game/forceloading.lua b/builtin/game/forceloading.lua index 7c5537e8..e1e00920 100644 --- a/builtin/game/forceloading.lua +++ b/builtin/game/forceloading.lua @@ -8,6 +8,9 @@ local blocks_forceloaded local blocks_temploaded = {} local total_forceloaded = 0 +-- true, if the forceloaded blocks got changed (flag for persistence on-disk) +local forceload_blocks_changed = false + local BLOCKSIZE = core.MAP_BLOCKSIZE local function get_blockpos(pos) return { @@ -31,6 +34,9 @@ local function get_relevant_tables(transient) end function core.forceload_block(pos, transient) + -- set changed flag + forceload_blocks_changed = true + local blockpos = get_blockpos(pos) local hash = core.hash_node_position(blockpos) local relevant_table, other_table = get_relevant_tables(transient) @@ -51,6 +57,9 @@ function core.forceload_block(pos, transient) end function core.forceload_free_block(pos, transient) + -- set changed flag + forceload_blocks_changed = true + local blockpos = get_blockpos(pos) local hash = core.hash_node_position(blockpos) local relevant_table, other_table = get_relevant_tables(transient) @@ -95,6 +104,28 @@ core.after(5, function() end end) -core.register_on_shutdown(function() +-- persists the currently forceloaded blocks to disk +local function persist_forceloaded_blocks() write_file(wpath.."/force_loaded.txt", blocks_forceloaded) -end) +end + +-- periodical forceload persistence +local function periodically_persist_forceloaded_blocks() + + -- only persist if the blocks actually changed + if forceload_blocks_changed then + persist_forceloaded_blocks() + + -- reset changed flag + forceload_blocks_changed = false + end + + -- recheck after some time + core.after(10, periodically_persist_forceloaded_blocks) +end + +-- persist periodically +core.after(5, periodically_persist_forceloaded_blocks) + +-- persist on shutdown +core.register_on_shutdown(persist_forceloaded_blocks) diff --git a/builtin/game/misc.lua b/builtin/game/misc.lua index 19ca9a02..43dca587 100644 --- a/builtin/game/misc.lua +++ b/builtin/game/misc.lua @@ -39,26 +39,41 @@ function core.check_player_privs(name, ...) return true, "" end + local player_list = {} -core.register_on_joinplayer(function(player) - local player_name = player:get_player_name() - player_list[player_name] = player - if not core.is_singleplayer() then - core.chat_send_all("=> " .. player_name .. " has joined the server") - end -end) -core.register_on_leaveplayer(function(player, timed_out) - local player_name = player:get_player_name() - player_list[player_name] = nil +function core.send_join_message(player_name) + core.chat_send_all("=> " .. player_name .. " has joined the server") +end + + +function core.send_leave_message(player_name, timed_out) local announcement = "<= " .. player_name .. " left the server" if timed_out then announcement = announcement .. " (timed out)" end core.chat_send_all(announcement) +end + + +core.register_on_joinplayer(function(player) + local player_name = player:get_player_name() + player_list[player_name] = player + if not core.is_singleplayer() then + core.send_join_message(player_name) + end + end) + +core.register_on_leaveplayer(function(player, timed_out) + local player_name = player:get_player_name() + player_list[player_name] = nil + core.send_leave_message(player_name, timed_out) +end) + + function core.get_connected_players() local temp_table = {} for _, value in pairs(player_list) do @@ -83,8 +98,10 @@ function core.player_exists(name) return core.get_auth_handler().get_auth(name) ~= nil end + -- Returns two position vectors representing a box of `radius` in each -- direction centered around the player corresponding to `player_name` + function core.get_player_radius_area(player_name, radius) local player = core.get_player_by_name(player_name) if player == nil then @@ -115,19 +132,23 @@ function core.is_valid_pos(pos) end function core.hash_node_position(pos) - return (pos.z+32768)*65536*65536 + (pos.y+32768)*65536 + pos.x+32768 + return (pos.z + 32768) * 65536 * 65536 + + (pos.y + 32768) * 65536 + + pos.x + 32768 end + function core.get_position_from_hash(hash) local pos = {} - pos.x = (hash%65536) - 32768 - hash = math.floor(hash/65536) - pos.y = (hash%65536) - 32768 - hash = math.floor(hash/65536) - pos.z = (hash%65536) - 32768 + pos.x = (hash % 65536) - 32768 + hash = math.floor(hash / 65536) + pos.y = (hash % 65536) - 32768 + hash = math.floor(hash / 65536) + pos.z = (hash % 65536) - 32768 return pos end + function core.get_item_group(name, group) if not core.registered_items[name] or not core.registered_items[name].groups[group] then @@ -136,11 +157,13 @@ function core.get_item_group(name, group) return core.registered_items[name].groups[group] end + function core.get_node_group(name, group) core.log("deprecated", "Deprecated usage of get_node_group, use get_item_group instead") return core.get_item_group(name, group) end + function core.setting_get_pos(name) local value = core.settings:get(name) if not value then @@ -150,11 +173,11 @@ function core.setting_get_pos(name) end -- To be overriden by protection mods + function core.is_protected() return false end --- To be overriden by protection mods function core.is_protected_action() return false end @@ -165,8 +188,8 @@ function core.record_protection_violation(pos, name) end end + -- Checks if specified volume intersects a protected volume --- Backport from Minetest 5.0 function core.is_area_protected(minp, maxp, player_name, interval) -- 'interval' is the largest allowed interval for the 3D lattice of checks. diff --git a/builtin/game/privileges.lua b/builtin/game/privileges.lua index 26d57b24..e46478ef 100644 --- a/builtin/game/privileges.lua +++ b/builtin/game/privileges.lua @@ -11,11 +11,14 @@ function core.register_privilege(name, param) if def.give_to_singleplayer == nil then def.give_to_singleplayer = true end + if def.give_to_admin == nil then + def.give_to_admin = def.give_to_singleplayer + end if def.description == nil then def.description = "(no description)" end end - local def = {} + local def if type(param) == "table" then def = param else @@ -36,7 +39,7 @@ core.register_privilege("basic_privs", "Can modify 'shout' and 'interact' privil core.register_privilege("privs", "Can modify privileges") core.register_privilege("teleport", { - description = "Can use /teleport command", + description = "Can teleport self", give_to_singleplayer = creative }) core.register_privilege("bring", { @@ -44,12 +47,13 @@ core.register_privilege("bring", { give_to_singleplayer = false }) core.register_privilege("settime", { - description = "Can use /time", + description = "Can set the time of day using /time", give_to_singleplayer = creative }) core.register_privilege("server", { description = "Can do server maintenance stuff", - give_to_singleplayer = false + give_to_singleplayer = false, + give_to_admin = true }) core.register_privilege("protection_bypass", { description = "Can bypass node protection in the world", @@ -57,11 +61,13 @@ core.register_privilege("protection_bypass", { }) core.register_privilege("ban", { description = "Can ban and unban players", - give_to_singleplayer = false + give_to_singleplayer = false, + give_to_admin = true }) core.register_privilege("kick", { description = "Can kick players", - give_to_singleplayer = false + give_to_singleplayer = false, + give_to_admin = true }) core.register_privilege("give", { description = "Can use /give and /giveme", @@ -72,15 +78,15 @@ core.register_privilege("password", { give_to_singleplayer = false }) core.register_privilege("fly", { - description = "Can fly using the free_move mode", + description = "Can use fly mode", give_to_singleplayer = creative }) core.register_privilege("fast", { - description = "Can walk fast using the fast_move mode", + description = "Can use fast mode", give_to_singleplayer = creative }) core.register_privilege("noclip", { - description = "Can fly through walls", + description = "Can fly through solid nodes using noclip mode", give_to_singleplayer = false }) core.register_privilege("rollback", { @@ -93,7 +99,8 @@ core.register_privilege("zoom", { }) core.register_privilege("debug", { description = "Allows enabling various debug options that may affect gameplay", - give_to_singleplayer = false + give_to_singleplayer = false, + give_to_admin = true }) core.register_privilege("weather", { description = "Allows changing the weather", diff --git a/builtin/game/register.lua b/builtin/game/register.lua index dbefd135..96a739d0 100644 --- a/builtin/game/register.lua +++ b/builtin/game/register.lua @@ -79,6 +79,7 @@ end function core.register_abm(spec) -- Add to core.registered_abms + assert(type(spec.action) == "function", "Required field 'action' of type function") core.registered_abms[#core.registered_abms + 1] = spec spec.mod_origin = core.get_current_modname() or "??" end @@ -86,6 +87,7 @@ end function core.register_lbm(spec) -- Add to core.registered_lbms check_modname_prefix(spec.name) + assert(type(spec.action) == "function", "Required field 'action' of type function") core.registered_lbms[#core.registered_lbms + 1] = spec spec.mod_origin = core.get_current_modname() or "??" end @@ -106,7 +108,7 @@ function core.register_entity(name, prototype) end -- Intllib -Sl = intllib.make_gettext_pair("locales"); +Sl = intllib.make_gettext_pair("locales") function core.register_item(name, itemdef) -- Check name @@ -185,7 +187,7 @@ function core.register_item(name, itemdef) --core.log("Registering item: " .. itemdef.name) core.registered_items[itemdef.name] = itemdef core.registered_aliases[itemdef.name] = nil - register_item_raw(itemdef) + register_item_raw(itemdef) end function core.unregister_item(name) @@ -309,18 +311,17 @@ end -- Alias the forbidden item names to "" so they can't be -- created via itemstrings (e.g. /give) -local name for name in pairs(forbidden_item_names) do core.registered_aliases[name] = "" register_alias_raw(name, "") end --- Deprecated: +-- Obsolete: -- Aliases for core.register_alias (how ironic...) ---core.alias_node = core.register_alias ---core.alias_tool = core.register_alias ---core.alias_craftitem = core.register_alias +-- core.alias_node = core.register_alias +-- core.alias_tool = core.register_alias +-- core.alias_craftitem = core.register_alias -- -- Built-in node definitions. Also defined in C. @@ -429,10 +430,6 @@ function core.run_callbacks(callbacks, mode, ...) local origin = core.callback_origins[callbacks[i]] if origin then core.set_last_run_mod(origin.mod) - --print("Running " .. tostring(callbacks[i]) .. - -- " (a " .. origin.name .. " callback in " .. origin.mod .. ")") - else - --print("No data associated with callback") end local cb_ret = callbacks[i](...) @@ -460,6 +457,18 @@ function core.run_callbacks(callbacks, mode, ...) return ret end +function core.run_priv_callbacks(name, priv, caller, method) + local def = core.registered_privileges[priv] + if not def or not def["on_" .. method] or + not def["on_" .. method](name, caller) then + for _, func in ipairs(core["registered_on_priv_" .. method]) do + if not func(name, caller, priv) then + break + end + end + end +end + -- -- Callback registration -- @@ -521,11 +530,11 @@ end core.registered_on_player_hpchanges = { modifiers = { }, loggers = { } } -function core.registered_on_player_hpchange(player, hp_change) - local last = false +function core.registered_on_player_hpchange(player, hp_change, reason) + local last for i = #core.registered_on_player_hpchanges.modifiers, 1, -1 do local func = core.registered_on_player_hpchanges.modifiers[i] - hp_change, last = func(player, hp_change) + hp_change, last = func(player, hp_change, reason) if type(hp_change) ~= "number" then local debuginfo = debug.getinfo(func) error("The register_on_hp_changes function has to return a number at " .. @@ -536,7 +545,7 @@ function core.registered_on_player_hpchange(player, hp_change) end end for i, func in ipairs(core.registered_on_player_hpchanges.loggers) do - func(player, hp_change) + func(player, hp_change, reason) end return hp_change end @@ -578,11 +587,12 @@ core.registered_craft_predicts, core.register_craft_predict = make_registration( core.registered_on_protection_violation, core.register_on_protection_violation = make_registration() core.registered_on_item_eats, core.register_on_item_eat = make_registration() core.registered_on_punchplayers, core.register_on_punchplayer = make_registration() +core.registered_on_priv_grant, core.register_on_priv_grant = make_registration() +core.registered_on_priv_revoke, core.register_on_priv_revoke = make_registration() -- Player step iteration -players_per_step = core.settings:get("players_per_globalstep") -players_per_step = players_per_step and tonumber(players_per_step) or 20 +players_per_step = tonumber(core.settings:get("players_per_globalstep")) or 20 local player_iter local player_iter_forced diff --git a/doc/lua_api.txt b/doc/lua_api.txt index 5c52793a..9e10612c 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -2234,45 +2234,101 @@ For the following functions `x` can be either a vector or a number: * Returns a scaled vector or Schur quotient. Helper functions ----------------- -* `dump2(obj, name="_", dumped={})` - * Return object serialized as a string, handles reference loops -* `dump(obj, dumped={})` - * Return object serialized as a string +----------------- +* `dump2(obj, name, dumped)`: returns a string which makes `obj` + human-readable, handles reference loops. + * `obj`: arbitrary variable + * `name`: string, default: `"_"` + * `dumped`: table, default: `{}` +* `dump(obj, dumped)`: returns a string which makes `obj` human-readable + * `obj`: arbitrary variable + * `dumped`: table, default: `{}` * `math.hypot(x, y)` * Get the hypotenuse of a triangle with legs x and y. Useful for distance calculation. -* `math.sign(x, tolerance)` +* `math.sign(x, tolerance)`: returns `-1`, `0` or `1` * Get the sign of a number. - Optional: Also returns `0` when the absolute value is within the tolerance (default: `0`) -* `string.split(str, separator=",", include_empty=false, max_splits=-1, sep_is_pattern=false)` - * If `max_splits` is negative, do not limit splits. - * `sep_is_pattern` specifies if separator is a plain string or a pattern (regex). - * e.g. `string:split("a,b", ",") == {"a","b"}` -* `string:trim()` - * e.g. `string.trim("\n \t\tfoo bar\t ") == "foo bar"` -* `minetest.wrap_text(str, limit, [as_table])`: returns a string or table - * Adds newlines to the string to keep it within the specified character limit - Note that returned lines may be longer than the limit since it only splits at word borders. - * limit: Maximal amount of characters in one line - * as_table: optional, if true return table of lines instead of string -* `minetest.pos_to_string({x=X,y=Y,z=Z}, decimal_places))`: returns string `"(X,Y,Z)"` - * Convert position to a printable string - Optional: 'decimal_places' will round the x, y and z of the pos to the given decimal place. -* `minetest.string_to_pos(string)`: returns a position - * Same but in reverse. Returns `nil` if the string can't be parsed to a position. + * tolerance: number, default: `0.0` + * If the absolute value of `x` is within the `tolerance` or `x` is NaN, + `0` is returned. +* `math.factorial(x)`: returns the factorial of `x` +* `string.split(str, separator, include_empty, max_splits, sep_is_pattern)` + * `separator`: string, default: `","` + * `include_empty`: boolean, default: `false` + * `max_splits`: number, if it's negative, splits aren't limited, + default: `-1` + * `sep_is_pattern`: boolean, it specifies whether separator is a plain + string or a pattern (regex), default: `false` + * e.g. `"a,b":split","` returns `{"a","b"}` +* `string:trim()`: returns the string without whitespace pre- and suffixes + * e.g. `"\n \t\tfoo bar\t ":trim()` returns `"foo bar"` +* `minetest.wrap_text(str, limit, as_table)`: returns a string or table + * Adds newlines to the string to keep it within the specified character + limit + * Note that the returned lines may be longer than the limit since it only + splits at word borders. + * `limit`: number, maximal amount of characters in one line + * `as_table`: boolean, if set to true, a table of lines instead of a string + is returned, default: `false` +* `minetest.pos_to_string(pos, decimal_places)`: returns string `"(X,Y,Z)"` + * `pos`: table {x=X, y=Y, z=Z} + * Converts the position `pos` to a human-readable, printable string + * `decimal_places`: number, if specified, the x, y and z values of + the position are rounded to the given decimal place. +* `minetest.string_to_pos(string)`: returns a position or `nil` + * Same but in reverse. + * If the string can't be parsed to a position, nothing is returned. * `minetest.string_to_area("(X1, Y1, Z1) (X2, Y2, Z2)")`: returns two positions * Converts a string representing an area box into two positions * `minetest.formspec_escape(string)`: returns a string - * escapes the characters "[", "]", "\", "," and ";", which can not be used in formspecs + * escapes the characters "[", "]", "\", "," and ";", which can not be used + in formspecs. * `minetest.is_yes(arg)` * returns true if passed 'y', 'yes', 'true' or a number that isn't zero. +* `minetest.is_nan(arg)` + * returns true when the passed number represents NaN. * `minetest.get_us_time()` * returns time with microsecond precision. May not return wall time. * `table.copy(table)`: returns a table * returns a deep copy of `table` -* `minetest.pointed_thing_to_face_pos(placer, pointed_thing)`: returns a position +* `table.indexof(list, val)`: returns the smallest numerical index containing + the value `val` in the table `list`. Non-numerical indices are ignored. + If `val` could not be found, `-1` is returned. `list` must not have + negative indices. +* `table.insert_all(table, other_table)`: + * Appends all values in `other_table` to `table` - uses `#table + 1` to + find new indices. +* `table.key_value_swap(t)`: returns a table with keys and values swapped + * If multiple keys in `t` map to the same value, the result is undefined. +* `table.shuffle(table, [from], [to], [random_func])`: + * Shuffles elements `from` to `to` in `table` in place + * `from` defaults to `1` + * `to` defaults to `#table` + * `random_func` defaults to `math.random`. This function receives two + integers as arguments and should return a random integer inclusively + between them. +* `minetest.pointed_thing_to_face_pos(placer, pointed_thing)`: returns a + position. * returns the exact position on the surface of a pointed node +* `minetest.get_dig_params(groups, tool_capabilities)`: Simulates a tool + that digs a node. + Returns a table with the following fields: + * `diggable`: `true` if node can be dug, `false` otherwise. + * `time`: Time it would take to dig the node. + * `wear`: How much wear would be added to the tool. + `time` and `wear` are meaningless if node's not diggable + Parameters: + * `groups`: Table of the node groups of the node that would be dug + * `tool_capabilities`: Tool capabilities table of the tool +* `minetest.get_hit_params(groups, tool_capabilities [, time_from_last_punch])`: + Simulates an item that punches an object. + Returns a table with the following fields: + * `hp`: How much damage the punch would cause. + * `wear`: How much wear would be added to the tool. + Parameters: + * `groups`: Damage groups of the object + * `tool_capabilities`: Tool capabilities table of the item + * `time_from_last_punch`: time in seconds since last punch action `minetest` namespace reference ------------------------------ @@ -2800,12 +2856,17 @@ and `minetest.auth_reload` call the authetification handler. * `{type="player", name="celeron55"}` * `{type="node", pos={x=, y=, z=}}` * `{type="detached", name="creative"}` -* `minetest.create_detached_inventory(name, callbacks, [player_name])`: returns an `InvRef` - * callbacks: See "Detached inventory callbacks" - * `player_name`: Make detached inventory available to one player exclusively, - by default they will be sent to every player (even if not used). - Note that this parameter is mostly just a workaround and will be removed in future releases. +* `minetest.create_detached_inventory(name, callbacks, [player_name])`: returns + an `InvRef`. + * `callbacks`: See [Detached inventory callbacks] + * `player_name`: Make detached inventory available to one player + exclusively, by default they will be sent to every player (even if not + used). + Note that this parameter is mostly just a workaround and will be removed + in future releases. * Creates a detached inventory. If it already exists, it is cleared. +* `minetest.remove_detached_inventory(name)` + * Returns a `boolean` indicating whether the removal succeeded. * `minetest.do_item_eat(hp_change, replace_with_item, poison, itemstack, user, pointed_thing)`: returns left over ItemStack * See `minetest.item_eat` and `minetest.register_on_item_eat` @@ -2912,8 +2973,8 @@ and `minetest.auth_reload` call the authetification handler. } * `minetest.handle_node_drops(pos, drops, digger)` * `drops`: list of itemstrings - * Handles drops from nodes after digging: Default action is to put them into - digger's inventory + * Handles drops from nodes after digging: Default action is to put them + into digger's inventory. * Can be overridden to get different functionality (e.g. dropping items on ground) * `minetest.itemstring_with_palette(item, palette_index)`: returns an item @@ -3166,6 +3227,10 @@ These functions return the leftover itemstack. * See documentation on `minetest.compress()` for supported compression methods. * currently supported. * `...` indicates method-specific arguments. Currently, no methods use this. +* `minetest.rgba(red, green, blue[, alpha])`: returns a string + * Each argument is a 8 Bit unsigned integer + * Returns the ColorString from rgb or rgba values + * Example: `minetest.rgba(10, 20, 30, 40)`, returns `"#0A141E28"` * `minetest.encode_base64(string)`: returns string encoded in base64 * Encodes a string in base64. * `minetest.decode_base64(string)`: returns string