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