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