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