]> git.lizzy.rs Git - minetest.git/blob - builtin/game/chatcommands.lua
Small fixes of minetest.has_feature
[minetest.git] / builtin / game / chatcommands.lua
1 -- Minetest: builtin/chatcommands.lua
2
3 --
4 -- Chat command handler
5 --
6
7 core.chatcommands = {}
8 function core.register_chatcommand(cmd, def)
9         def = def or {}
10         def.params = def.params or ""
11         def.description = def.description or ""
12         def.privs = def.privs or {}
13         def.mod_origin = core.get_current_modname() or "??"
14         core.chatcommands[cmd] = def
15 end
16
17 if core.setting_getbool("mod_profiling") then
18         local tracefct = profiling_print_log
19         profiling_print_log = nil
20         core.register_chatcommand("save_mod_profile",
21                         {
22                                 params      = "",
23                                 description = "save mod profiling data to logfile " ..
24                                                 "(depends on default loglevel)",
25                                 func        = tracefct,
26                                 privs       = { server=true }
27                         })
28 end
29
30 core.register_on_chat_message(function(name, message)
31         local cmd, param = string.match(message, "^/([^ ]+) *(.*)")
32         if not param then
33                 param = ""
34         end
35         local cmd_def = core.chatcommands[cmd]
36         if not cmd_def then
37                 return false
38         end
39         local has_privs, missing_privs = core.check_player_privs(name, cmd_def.privs)
40         if has_privs then
41                 core.set_last_run_mod(cmd_def.mod_origin)
42                 local success, message = cmd_def.func(name, param)
43                 if message then
44                         core.chat_send_player(name, message)
45                 end
46         else
47                 core.chat_send_player(name, "You don't have permission"
48                                 .. " to run this command (missing privileges: "
49                                 .. table.concat(missing_privs, ", ") .. ")")
50         end
51         return true  -- Handled chat message
52 end)
53
54 --
55 -- Chat commands
56 --
57 core.register_chatcommand("me", {
58         params = "<action>",
59         description = "chat action (eg. /me orders a pizza)",
60         privs = {shout=true},
61         func = function(name, param)
62                 core.chat_send_all("* " .. name .. " " .. param)
63         end,
64 })
65
66 core.register_chatcommand("help", {
67         privs = {},
68         params = "[all/privs/<cmd>]",
69         description = "Get help for commands or list privileges",
70         func = function(name, param)
71                 local function format_help_line(cmd, def)
72                         local msg = "/"..cmd
73                         if def.params and def.params ~= "" then
74                                 msg = msg .. " " .. def.params
75                         end
76                         if def.description and def.description ~= "" then
77                                 msg = msg .. ": " .. def.description
78                         end
79                         return msg
80                 end
81                 if param == "" then
82                         local msg = ""
83                         local cmds = {}
84                         for cmd, def in pairs(core.chatcommands) do
85                                 if core.check_player_privs(name, def.privs) then
86                                         table.insert(cmds, cmd)
87                                 end
88                         end
89                         table.sort(cmds)
90                         return true, "Available commands: " .. table.concat(cmds, " ") .. "\n"
91                                         .. "Use '/help <cmd>' to get more information,"
92                                         .. " or '/help all' to list everything."
93                 elseif param == "all" then
94                         local cmds = {}
95                         for cmd, def in pairs(core.chatcommands) do
96                                 if core.check_player_privs(name, def.privs) then
97                                         table.insert(cmds, format_help_line(cmd, def))
98                                 end
99                         end
100                         table.sort(cmds)
101                         return true, "Available commands:\n"..table.concat(cmds, "\n")
102                 elseif param == "privs" then
103                         local privs = {}
104                         for priv, def in pairs(core.registered_privileges) do
105                                 table.insert(privs, priv .. ": " .. def.description)
106                         end
107                         table.sort(privs)
108                         return true, "Available privileges:\n"..table.concat(privs, "\n")
109                 else
110                         local cmd = param
111                         local def = core.chatcommands[cmd]
112                         if not def then
113                                 return false, "Command not available: "..cmd
114                         else
115                                 return true, format_help_line(cmd, def)
116                         end
117                 end
118         end,
119 })
120
121 core.register_chatcommand("privs", {
122         params = "<name>",
123         description = "print out privileges of player",
124         func = function(name, param)
125                 param = (param ~= "" and param or name)
126                 return true, "Privileges of " .. param .. ": "
127                         .. core.privs_to_string(
128                                 core.get_player_privs(param), ' ')
129         end,
130 })
131 core.register_chatcommand("grant", {
132         params = "<name> <privilege>|all",
133         description = "Give privilege to player",
134         func = function(name, param)
135                 if not core.check_player_privs(name, {privs=true}) and
136                                 not core.check_player_privs(name, {basic_privs=true}) then
137                         return false, "Your privileges are insufficient."
138                 end
139                 local grantname, grantprivstr = string.match(param, "([^ ]+) (.+)")
140                 if not grantname or not grantprivstr then
141                         return false, "Invalid parameters (see /help grant)"
142                 elseif not core.auth_table[grantname] then
143                         return false, "Player " .. grantname .. " does not exist."
144                 end
145                 local grantprivs = core.string_to_privs(grantprivstr)
146                 if grantprivstr == "all" then
147                         grantprivs = core.registered_privileges
148                 end
149                 local privs = core.get_player_privs(grantname)
150                 local privs_unknown = ""
151                 for priv, _ in pairs(grantprivs) do
152                         if priv ~= "interact" and priv ~= "shout" and
153                                         not core.check_player_privs(name, {privs=true}) then
154                                 return false, "Your privileges are insufficient."
155                         end
156                         if not core.registered_privileges[priv] then
157                                 privs_unknown = privs_unknown .. "Unknown privilege: " .. priv .. "\n"
158                         end
159                         privs[priv] = true
160                 end
161                 if privs_unknown ~= "" then
162                         return false, privs_unknown
163                 end
164                 core.set_player_privs(grantname, privs)
165                 core.log("action", name..' granted ('..core.privs_to_string(grantprivs, ', ')..') privileges to '..grantname)
166                 if grantname ~= name then
167                         core.chat_send_player(grantname, name
168                                         .. " granted you privileges: "
169                                         .. core.privs_to_string(grantprivs, ' '))
170                 end
171                 return true, "Privileges of " .. grantname .. ": "
172                         .. core.privs_to_string(
173                                 core.get_player_privs(grantname), ' ')
174         end,
175 })
176 core.register_chatcommand("revoke", {
177         params = "<name> <privilege>|all",
178         description = "Remove privilege from player",
179         privs = {},
180         func = function(name, param)
181                 if not core.check_player_privs(name, {privs=true}) and
182                                 not core.check_player_privs(name, {basic_privs=true}) then
183                         return false, "Your privileges are insufficient."
184                 end
185                 local revoke_name, revoke_priv_str = string.match(param, "([^ ]+) (.+)")
186                 if not revoke_name or not revoke_priv_str then
187                         return false, "Invalid parameters (see /help revoke)"
188                 elseif not core.auth_table[revoke_name] then
189                         return false, "Player " .. revoke_name .. " does not exist."
190                 end
191                 local revoke_privs = core.string_to_privs(revoke_priv_str)
192                 local privs = core.get_player_privs(revoke_name)
193                 for priv, _ in pairs(revoke_privs) do
194                         if priv ~= "interact" and priv ~= "shout" and
195                                         not core.check_player_privs(name, {privs=true}) then
196                                 return false, "Your privileges are insufficient."
197                         end
198                 end
199                 if revoke_priv_str == "all" then
200                         privs = {}
201                 else
202                         for priv, _ in pairs(revoke_privs) do
203                                 privs[priv] = nil
204                         end
205                 end
206                 core.set_player_privs(revoke_name, privs)
207                 core.log("action", name..' revoked ('
208                                 ..core.privs_to_string(revoke_privs, ', ')
209                                 ..') privileges from '..revoke_name)
210                 if revoke_name ~= name then
211                         core.chat_send_player(revoke_name, name
212                                         .. " revoked privileges from you: "
213                                         .. core.privs_to_string(revoke_privs, ' '))
214                 end
215                 return true, "Privileges of " .. revoke_name .. ": "
216                         .. core.privs_to_string(
217                                 core.get_player_privs(revoke_name), ' ')
218         end,
219 })
220
221 core.register_chatcommand("setpassword", {
222         params = "<name> <password>",
223         description = "set given password",
224         privs = {password=true},
225         func = function(name, param)
226                 local toname, raw_password = string.match(param, "^([^ ]+) +(.+)$")
227                 if not toname then
228                         toname = param:match("^([^ ]+) *$")
229                         raw_password = nil
230                 end
231                 if not toname then
232                         return false, "Name field required"
233                 end
234                 local act_str_past = "?"
235                 local act_str_pres = "?"
236                 if not raw_password then
237                         core.set_player_password(toname, "")
238                         act_str_past = "cleared"
239                         act_str_pres = "clears"
240                 else
241                         core.set_player_password(toname,
242                                         core.get_password_hash(toname,
243                                                         raw_password))
244                         act_str_past = "set"
245                         act_str_pres = "sets"
246                 end
247                 if toname ~= name then
248                         core.chat_send_player(toname, "Your password was "
249                                         .. act_str_past .. " by " .. name)
250                 end
251
252                 core.log("action", name .. " " .. act_str_pres
253                 .. " password of " .. toname .. ".")
254
255                 return true, "Password of player \"" .. toname .. "\" " .. act_str_past
256         end,
257 })
258
259 core.register_chatcommand("clearpassword", {
260         params = "<name>",
261         description = "set empty password",
262         privs = {password=true},
263         func = function(name, param)
264                 local toname = param
265                 if toname == "" then
266                         return false, "Name field required"
267                 end
268                 core.set_player_password(toname, '')
269
270                 core.log("action", name .. " clears password of " .. toname .. ".")
271
272                 return true, "Password of player \"" .. toname .. "\" cleared"
273         end,
274 })
275
276 core.register_chatcommand("auth_reload", {
277         params = "",
278         description = "reload authentication data",
279         privs = {server=true},
280         func = function(name, param)
281                 local done = core.auth_reload()
282                 return done, (done and "Done." or "Failed.")
283         end,
284 })
285
286 core.register_chatcommand("teleport", {
287         params = "<X>,<Y>,<Z> | <to_name> | <name> <X>,<Y>,<Z> | <name> <to_name>",
288         description = "teleport to given position",
289         privs = {teleport=true},
290         func = function(name, param)
291                 -- Returns (pos, true) if found, otherwise (pos, false)
292                 local function find_free_position_near(pos)
293                         local tries = {
294                                 {x=1,y=0,z=0},
295                                 {x=-1,y=0,z=0},
296                                 {x=0,y=0,z=1},
297                                 {x=0,y=0,z=-1},
298                         }
299                         for _, d in ipairs(tries) do
300                                 local p = {x = pos.x+d.x, y = pos.y+d.y, z = pos.z+d.z}
301                                 local n = core.get_node_or_nil(p)
302                                 if n and n.name then
303                                         local def = core.registered_nodes[n.name]
304                                         if def and not def.walkable then
305                                                 return p, true
306                                         end
307                                 end
308                         end
309                         return pos, false
310                 end
311
312                 local teleportee = nil
313                 local p = {}
314                 p.x, p.y, p.z = string.match(param, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
315                 p.x = tonumber(p.x)
316                 p.y = tonumber(p.y)
317                 p.z = tonumber(p.z)
318                 teleportee = core.get_player_by_name(name)
319                 if teleportee and p.x and p.y and p.z then
320                         teleportee:setpos(p)
321                         return true, "Teleporting to "..core.pos_to_string(p)
322                 end
323
324                 local teleportee = nil
325                 local p = nil
326                 local target_name = nil
327                 target_name = param:match("^([^ ]+)$")
328                 teleportee = core.get_player_by_name(name)
329                 if target_name then
330                         local target = core.get_player_by_name(target_name)
331                         if target then
332                                 p = target:getpos()
333                         end
334                 end
335                 if teleportee and p then
336                         p = find_free_position_near(p)
337                         teleportee:setpos(p)
338                         return true, "Teleporting to " .. target_name
339                                         .. " at "..core.pos_to_string(p)
340                 end
341
342                 if not core.check_player_privs(name, {bring=true}) then
343                         return false, "You don't have permission to teleport other players (missing bring privilege)"
344                 end
345
346                 local teleportee = nil
347                 local p = {}
348                 local teleportee_name = nil
349                 teleportee_name, p.x, p.y, p.z = param:match(
350                                 "^([^ ]+) +([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
351                 p.x, p.y, p.z = tonumber(p.x), tonumber(p.y), tonumber(p.z)
352                 if teleportee_name then
353                         teleportee = core.get_player_by_name(teleportee_name)
354                 end
355                 if teleportee and p.x and p.y and p.z then
356                         teleportee:setpos(p)
357                         return true, "Teleporting " .. teleportee_name
358                                         .. " to " .. core.pos_to_string(p)
359                 end
360
361                 local teleportee = nil
362                 local p = nil
363                 local teleportee_name = nil
364                 local target_name = nil
365                 teleportee_name, target_name = string.match(param, "^([^ ]+) +([^ ]+)$")
366                 if teleportee_name then
367                         teleportee = core.get_player_by_name(teleportee_name)
368                 end
369                 if target_name then
370                         local target = core.get_player_by_name(target_name)
371                         if target then
372                                 p = target:getpos()
373                         end
374                 end
375                 if teleportee and p then
376                         p = find_free_position_near(p)
377                         teleportee:setpos(p)
378                         return true, "Teleporting " .. teleportee_name
379                                         .. " to " .. target_name
380                                         .. " at " .. core.pos_to_string(p)
381                 end
382
383                 return false, 'Invalid parameters ("' .. param
384                                 .. '") or player not found (see /help teleport)'
385         end,
386 })
387
388 core.register_chatcommand("set", {
389         params = "[-n] <name> <value> | <name>",
390         description = "set or read server configuration setting",
391         privs = {server=true},
392         func = function(name, param)
393                 local arg, setname, setvalue = string.match(param, "(-[n]) ([^ ]+) (.+)")
394                 if arg and arg == "-n" and setname and setvalue then
395                         core.setting_set(setname, setvalue)
396                         return true, setname .. " = " .. setvalue
397                 end
398                 local setname, setvalue = string.match(param, "([^ ]+) (.+)")
399                 if setname and setvalue then
400                         if not core.setting_get(setname) then
401                                 return false, "Failed. Use '/set -n <name> <value>' to create a new setting."
402                         end
403                         core.setting_set(setname, setvalue)
404                         return true, setname .. " = " .. setvalue
405                 end
406                 local setname = string.match(param, "([^ ]+)")
407                 if setname then
408                         local setvalue = core.setting_get(setname)
409                         if not setvalue then
410                                 setvalue = "<not set>"
411                         end
412                         return true, setname .. " = " .. setvalue
413                 end
414                 return false, "Invalid parameters (see /help set)."
415         end,
416 })
417
418 core.register_chatcommand("deleteblocks", {
419         params = "(here [radius]) | (<pos1> <pos2>)",
420         description = "delete map blocks contained in area pos1 to pos2",
421         privs = {server=true},
422         func = function(name, param)
423                 local p1 = {}
424                 local p2 = {}
425                 local args = param:split(" ")
426                 if args[1] == "here" then
427                         local player = core.get_player_by_name(name)
428                         if player == nil then
429                                 core.log("error", "player is nil")
430                                 return false, "Unable to get current position; player is nil"
431                         end
432                         p1 = player:getpos()
433                         p2 = p1
434
435                         if #args >= 2 then
436                                 local radius = tonumber(args[2]) or 0
437                                 p1 = vector.add(p1, radius)
438                                 p2 = vector.subtract(p2, radius)
439                         end
440                 else
441                         local pos1, pos2 = unpack(param:split(") ("))
442                         if pos1 == nil or pos2 == nil then
443                                 return false, "Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)"
444                         end
445
446                         p1 = core.string_to_pos(pos1 .. ")")
447                         p2 = core.string_to_pos("(" .. pos2)
448
449                         if p1 == nil or p2 == nil then
450                                 return false, "Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)"
451                         end
452                 end
453
454                 if core.delete_area(p1, p2) then
455                         return true, "Successfully cleared area ranging from " ..
456                                 core.pos_to_string(p1, 1) .. " to " .. core.pos_to_string(p2, 1)
457                 else
458                         return false, "Failed to clear one or more blocks in area"
459                 end
460         end,
461 })
462
463 core.register_chatcommand("mods", {
464         params = "",
465         description = "List mods installed on the server",
466         privs = {},
467         func = function(name, param)
468                 return true, table.concat(core.get_modnames(), ", ")
469         end,
470 })
471
472 local function handle_give_command(cmd, giver, receiver, stackstring)
473         core.log("action", giver .. " invoked " .. cmd
474                         .. ', stackstring="' .. stackstring .. '"')
475         local itemstack = ItemStack(stackstring)
476         if itemstack:is_empty() then
477                 return false, "Cannot give an empty item"
478         elseif not itemstack:is_known() then
479                 return false, "Cannot give an unknown item"
480         end
481         local receiverref = core.get_player_by_name(receiver)
482         if receiverref == nil then
483                 return false, receiver .. " is not a known player"
484         end
485         local leftover = receiverref:get_inventory():add_item("main", itemstack)
486         local partiality
487         if leftover:is_empty() then
488                 partiality = ""
489         elseif leftover:get_count() == itemstack:get_count() then
490                 partiality = "could not be "
491         else
492                 partiality = "partially "
493         end
494         -- The actual item stack string may be different from what the "giver"
495         -- entered (e.g. big numbers are always interpreted as 2^16-1).
496         stackstring = itemstack:to_string()
497         if giver == receiver then
498                 return true, ("%q %sadded to inventory.")
499                                 :format(stackstring, partiality)
500         else
501                 core.chat_send_player(receiver, ("%q %sadded to inventory.")
502                                 :format(stackstring, partiality))
503                 return true, ("%q %sadded to %s's inventory.")
504                                 :format(stackstring, partiality, receiver)
505         end
506 end
507
508 core.register_chatcommand("give", {
509         params = "<name> <ItemString>",
510         description = "give item to player",
511         privs = {give=true},
512         func = function(name, param)
513                 local toname, itemstring = string.match(param, "^([^ ]+) +(.+)$")
514                 if not toname or not itemstring then
515                         return false, "Name and ItemString required"
516                 end
517                 return handle_give_command("/give", name, toname, itemstring)
518         end,
519 })
520
521 core.register_chatcommand("giveme", {
522         params = "<ItemString>",
523         description = "give item to yourself",
524         privs = {give=true},
525         func = function(name, param)
526                 local itemstring = string.match(param, "(.+)$")
527                 if not itemstring then
528                         return false, "ItemString required"
529                 end
530                 return handle_give_command("/giveme", name, name, itemstring)
531         end,
532 })
533
534 core.register_chatcommand("spawnentity", {
535         params = "<EntityName> [<X>,<Y>,<Z>]",
536         description = "Spawn entity at given (or your) position",
537         privs = {give=true, interact=true},
538         func = function(name, param)
539                 local entityname, p = string.match(param, "^([^ ]+) *(.*)$")
540                 if not entityname then
541                         return false, "EntityName required"
542                 end
543                 core.log("action", ("%s invokes /spawnentity, entityname=%q")
544                                 :format(name, entityname))
545                 local player = core.get_player_by_name(name)
546                 if player == nil then
547                         core.log("error", "Unable to spawn entity, player is nil")
548                         return false, "Unable to spawn entity, player is nil"
549                 end
550                 if p == "" then
551                         p = player:getpos()
552                 else
553                         p = core.string_to_pos(p)
554                         if p == nil then
555                                 return false, "Invalid parameters ('" .. param .. "')"
556                         end
557                 end
558                 p.y = p.y + 1
559                 core.add_entity(p, entityname)
560                 return true, ("%q spawned."):format(entityname)
561         end,
562 })
563
564 core.register_chatcommand("pulverize", {
565         params = "",
566         description = "Destroy item in hand",
567         func = function(name, param)
568                 local player = core.get_player_by_name(name)
569                 if not player then
570                         core.log("error", "Unable to pulverize, no player.")
571                         return false, "Unable to pulverize, no player."
572                 end
573                 if player:get_wielded_item():is_empty() then
574                         return false, "Unable to pulverize, no item in hand."
575                 end
576                 player:set_wielded_item(nil)
577                 return true, "An item was pulverized."
578         end,
579 })
580
581 -- Key = player name
582 core.rollback_punch_callbacks = {}
583
584 core.register_on_punchnode(function(pos, node, puncher)
585         local name = puncher:get_player_name()
586         if core.rollback_punch_callbacks[name] then
587                 core.rollback_punch_callbacks[name](pos, node, puncher)
588                 core.rollback_punch_callbacks[name] = nil
589         end
590 end)
591
592 core.register_chatcommand("rollback_check", {
593         params = "[<range>] [<seconds>] [limit]",
594         description = "Check who has last touched a node or near it,"
595                         .. " max. <seconds> ago (default range=0,"
596                         .. " seconds=86400=24h, limit=5)",
597         privs = {rollback=true},
598         func = function(name, param)
599                 if not core.setting_getbool("enable_rollback_recording") then
600                         return false, "Rollback functions are disabled."
601                 end
602                 local range, seconds, limit =
603                         param:match("(%d+) *(%d*) *(%d*)")
604                 range = tonumber(range) or 0
605                 seconds = tonumber(seconds) or 86400
606                 limit = tonumber(limit) or 5
607                 if limit > 100 then
608                         return false, "That limit is too high!"
609                 end
610
611                 core.rollback_punch_callbacks[name] = function(pos, node, puncher)
612                         local name = puncher:get_player_name()
613                         core.chat_send_player(name, "Checking " .. core.pos_to_string(pos) .. "...")
614                         local actions = core.rollback_get_node_actions(pos, range, seconds, limit)
615                         if not actions then
616                                 core.chat_send_player(name, "Rollback functions are disabled")
617                                 return
618                         end
619                         local num_actions = #actions
620                         if num_actions == 0 then
621                                 core.chat_send_player(name, "Nobody has touched"
622                                                 .. " the specified location in "
623                                                 .. seconds .. " seconds")
624                                 return
625                         end
626                         local time = os.time()
627                         for i = num_actions, 1, -1 do
628                                 local action = actions[i]
629                                 core.chat_send_player(name,
630                                         ("%s %s %s -> %s %d seconds ago.")
631                                                 :format(
632                                                         core.pos_to_string(action.pos),
633                                                         action.actor,
634                                                         action.oldnode.name,
635                                                         action.newnode.name,
636                                                         time - action.time))
637                         end
638                 end
639
640                 return true, "Punch a node (range=" .. range .. ", seconds="
641                                 .. seconds .. "s, limit=" .. limit .. ")"
642         end,
643 })
644
645 core.register_chatcommand("rollback", {
646         params = "<player name> [<seconds>] | :<actor> [<seconds>]",
647         description = "revert actions of a player; default for <seconds> is 60",
648         privs = {rollback=true},
649         func = function(name, param)
650                 if not core.setting_getbool("enable_rollback_recording") then
651                         return false, "Rollback functions are disabled."
652                 end
653                 local target_name, seconds = string.match(param, ":([^ ]+) *(%d*)")
654                 if not target_name then
655                         local player_name = nil
656                         player_name, seconds = string.match(param, "([^ ]+) *(%d*)")
657                         if not player_name then
658                                 return false, "Invalid parameters. See /help rollback"
659                                                 .. " and /help rollback_check."
660                         end
661                         target_name = "player:"..player_name
662                 end
663                 seconds = tonumber(seconds) or 60
664                 core.chat_send_player(name, "Reverting actions of "
665                                 .. target_name .. " since "
666                                 .. seconds .. " seconds.")
667                 local success, log = core.rollback_revert_actions_by(
668                                 target_name, seconds)
669                 local response = ""
670                 if #log > 100 then
671                         response = "(log is too long to show)\n"
672                 else
673                         for _, line in pairs(log) do
674                                 response = response .. line .. "\n"
675                         end
676                 end
677                 response = response .. "Reverting actions "
678                                 .. (success and "succeeded." or "FAILED.")
679                 return success, response
680         end,
681 })
682
683 core.register_chatcommand("status", {
684         description = "Print server status",
685         func = function(name, param)
686                 return true, core.get_server_status()
687         end,
688 })
689
690 core.register_chatcommand("time", {
691         params = "<0..23>:<0..59> | <0..24000>",
692         description = "set time of day",
693         privs = {},
694         func = function(name, param)
695                 if param == "" then
696                         local current_time = math.floor(core.get_timeofday() * 1440)
697                         local minutes = current_time % 60
698                         local hour = (current_time - minutes) / 60
699                         return true, ("Current time is %d:%02d"):format(hour, minutes)
700                 end
701                 local player_privs = minetest.get_player_privs(name)
702                 if not player_privs.settime then
703                         return false, "You don't have permission to run this command " ..
704                                 "(missing privilege: settime)."
705                 end
706                 local hour, minute = param:match("^(%d+):(%d+)$")
707                 if not hour then
708                         local new_time = tonumber(param)
709                         if not new_time then
710                                 return false, "Invalid time."
711                         end
712                         -- Backward compatibility.
713                         core.set_timeofday((new_time % 24000) / 24000)
714                         core.log("action", name .. " sets time to " .. new_time)
715                         return true, "Time of day changed."
716                 end
717                 hour = tonumber(hour)
718                 minute = tonumber(minute)
719                 if hour < 0 or hour > 23 then
720                         return false, "Invalid hour (must be between 0 and 23 inclusive)."
721                 elseif minute < 0 or minute > 59 then
722                         return false, "Invalid minute (must be between 0 and 59 inclusive)."
723                 end
724                 core.set_timeofday((hour * 60 + minute) / 1440)
725                 core.log("action", ("%s sets time to %d:%02d"):format(name, hour, minute))
726                 return true, "Time of day changed."
727         end,
728 })
729
730 core.register_chatcommand("shutdown", {
731         description = "shutdown server",
732         privs = {server=true},
733         func = function(name, param)
734                 core.log("action", name .. " shuts down server")
735                 core.request_shutdown()
736                 core.chat_send_all("*** Server shutting down (operator request).")
737         end,
738 })
739
740 core.register_chatcommand("ban", {
741         params = "<name>",
742         description = "Ban IP of player",
743         privs = {ban=true},
744         func = function(name, param)
745                 if param == "" then
746                         return true, "Ban list: " .. core.get_ban_list()
747                 end
748                 if not core.get_player_by_name(param) then
749                         return false, "No such player."
750                 end
751                 if not core.ban_player(param) then
752                         return false, "Failed to ban player."
753                 end
754                 local desc = core.get_ban_description(param)
755                 core.log("action", name .. " bans " .. desc .. ".")
756                 return true, "Banned " .. desc .. "."
757         end,
758 })
759
760 core.register_chatcommand("unban", {
761         params = "<name/ip>",
762         description = "remove IP ban",
763         privs = {ban=true},
764         func = function(name, param)
765                 if not core.unban_player_or_ip(param) then
766                         return false, "Failed to unban player/IP."
767                 end
768                 core.log("action", name .. " unbans " .. param)
769                 return true, "Unbanned " .. param
770         end,
771 })
772
773 core.register_chatcommand("kick", {
774         params = "<name> [reason]",
775         description = "kick a player",
776         privs = {kick=true},
777         func = function(name, param)
778                 local tokick, reason = param:match("([^ ]+) (.+)")
779                 tokick = tokick or param
780                 if not core.kick_player(tokick, reason) then
781                         return false, "Failed to kick player " .. tokick
782                 end
783                 local log_reason = ""
784                 if reason then
785                         log_reason = " with reason \"" .. reason .. "\""
786                 end
787                 core.log("action", name .. " kicks " .. tokick .. log_reason)
788                 return true, "Kicked " .. tokick
789         end,
790 })
791
792 core.register_chatcommand("clearobjects", {
793         description = "clear all objects in world",
794         privs = {server=true},
795         func = function(name, param)
796                 core.log("action", name .. " clears all objects.")
797                 core.chat_send_all("Clearing all objects.  This may take long."
798                                 .. "  You may experience a timeout.  (by "
799                                 .. name .. ")")
800                 core.clear_objects()
801                 core.log("action", "Object clearing done.")
802                 core.chat_send_all("*** Cleared all objects.")
803         end,
804 })
805
806 core.register_chatcommand("msg", {
807         params = "<name> <message>",
808         description = "Send a private message",
809         privs = {shout=true},
810         func = function(name, param)
811                 local sendto, message = param:match("^(%S+)%s(.+)$")
812                 if not sendto then
813                         return false, "Invalid usage, see /help msg."
814                 end
815                 if not core.get_player_by_name(sendto) then
816                         return false, "The player " .. sendto
817                                         .. " is not online."
818                 end
819                 core.log("action", "PM from " .. name .. " to " .. sendto
820                                 .. ": " .. message)
821                 core.chat_send_player(sendto, "PM from " .. name .. ": "
822                                 .. message)
823                 return true, "Message sent."
824         end,
825 })
826
827 core.register_chatcommand("last-login", {
828         params = "[name]",
829         description = "Get the last login time of a player",
830         func = function(name, param)
831                 if param == "" then
832                         param = name
833                 end
834                 local pauth = core.get_auth_handler().get_auth(param)
835                 if pauth and pauth.last_login then
836                         -- Time in UTC, ISO 8601 format
837                         return true, "Last login time was " ..
838                                 os.date("!%Y-%m-%dT%H:%M:%SZ", pauth.last_login)
839                 end
840                 return false, "Last login time is unknown"
841         end,
842 })