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