380 lines
12 KiB
Lua
380 lines
12 KiB
Lua
-- LUALOCALS < ---------------------------------------------------------
|
|
local io, ipairs, math, minetest, pairs, string, table, tonumber
|
|
= io, ipairs, math, minetest, pairs, string, table, tonumber
|
|
local io_open, math_floor, string_format, string_match, table_concat,
|
|
table_insert
|
|
= io.open, math.floor, string.format, string.match, table.concat,
|
|
table.insert
|
|
-- LUALOCALS > ---------------------------------------------------------
|
|
|
|
local modname = minetest.get_current_modname()
|
|
local modstore = minetest.get_mod_storage()
|
|
|
|
------------------------------------------------------------------------
|
|
-- Global configuration
|
|
|
|
local function conf(n)
|
|
return minetest.settings:get(modname .. "_" .. n)
|
|
end
|
|
|
|
-- Default restart grace time.
|
|
local grace = tonumber(conf("grace")) or 300
|
|
|
|
-- Amount of time before a restart that the countdown will be
|
|
-- announced/displayed.
|
|
local countdown = conf("countdown") or grace
|
|
|
|
-- Amount of time during which the countdown will flash.
|
|
local critical = conf("countdown") or 10
|
|
|
|
-- Primary color of the countdown HUD.
|
|
local hudcolor = conf("hudcolor") or 0xFFFF00
|
|
|
|
-- Flashing color of HUD during critical countdown.
|
|
local hudcolorflash = conf("hudcolor") or 0xFF0000
|
|
|
|
-- Don't restart the server too often; give players at least this
|
|
-- much time after a restart, if any are on.
|
|
local minuptime = tonumber(conf("minuptime")) or 7200
|
|
|
|
-- Always restart after the server has been up this long.
|
|
local maxuptime = tonumber(conf("maxuptime")) or 86400
|
|
|
|
-- Always shut down the server as soon as it becomes empty, even
|
|
-- if no shutdown was requested for other reasons.
|
|
local always = minetest.settings:get_bool(modname .. "_always") or nil
|
|
|
|
-- Restart shutdown message.
|
|
local shtudownmsg = string_format("\n\n%s\n%s",
|
|
conf("shutdownmsg1") or "SERVER RESTARTING FOR UPDATES",
|
|
conf("shutdownmsg2") or "Please reconnect in about 10 seconds")
|
|
|
|
-- Message when players are kicked off for restart
|
|
local kickmsg = conf("kickmsg") or "*** Kicking players off for restart"
|
|
|
|
-- Message when restarting but there are no players online
|
|
local restartmsg = conf("restartmsg") or "*** Restarting server"
|
|
|
|
-- Message when server has restarted after an announced restart
|
|
local completemsg = conf("completemsg") or "*** Restart complete"
|
|
|
|
-- Delay before sending restart complete message, unless a player
|
|
-- connects first, to give chat bridges a little time to spin up.
|
|
local completedelay = tonumber(conf("completedelay")) or 1
|
|
|
|
-- How long before the restart the first announcement is made in chat.
|
|
local announcetime = tonumber(conf("announcetime")) or 7200
|
|
|
|
-- Message used to announce pending restart in chat.
|
|
local chatmsg = conf("chatmsg") or "*** Server restart in %s"
|
|
|
|
-- Message used to display pending restart in HUD.
|
|
local hudmsg = conf("hudmsg") or "RESTART IN %s"
|
|
|
|
------------------------------------------------------------------------
|
|
-- Global time functions
|
|
|
|
-- Function to get current uptime of server.
|
|
local uptime
|
|
do
|
|
local starttime = minetest.get_us_time()
|
|
uptime = function()
|
|
return (minetest.get_us_time() - starttime) / 1000000
|
|
end
|
|
end
|
|
|
|
-- If restart requested, this is the uptime value at which the
|
|
-- restart should happen, nil if none pending.
|
|
local req
|
|
|
|
-- Get number of seconds until restart, if any.
|
|
local function remain()
|
|
return req and req - uptime()
|
|
end
|
|
|
|
-- Get formatted restart time, if any
|
|
local function remaintext()
|
|
if not req then return end
|
|
local cdown = math_floor(remain())
|
|
if cdown <= 0 then return ":00" end
|
|
local min = cdown / 60
|
|
local sec = cdown % 60
|
|
if min < 60 then return string_format("%d:%02d", min, sec) end
|
|
local hr = min / 60
|
|
min = min % 60
|
|
return string_format("%d:%02d:%02d", hr, min, sec)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Online player functions
|
|
|
|
-- Special privilege to indicate this user does NOT count toward the
|
|
-- user count for purposes of keeping the server alive when a shutdown
|
|
-- is pending; for bot users and the like who should be booted when
|
|
-- the human players are all gone.
|
|
local ignorepriv = modname .. "_ignore"
|
|
minetest.register_privilege(ignorepriv, {
|
|
description = "server can shut down early when this user is online",
|
|
give_to_admin = false,
|
|
give_to_singleplayer = false
|
|
})
|
|
|
|
-- Exclude the restart ignore priv specifically from the grant "all" chat
|
|
-- commands so that "/grantme all" on a singleplayer world with this mod
|
|
-- installed won't immediately shut the world down as the player suddenly
|
|
-- instantly becomes ignored.
|
|
local function grantwrap(key, skip)
|
|
local cmd = minetest.registered_chatcommands[key]
|
|
if not cmd then return end
|
|
local oldfunc = cmd.func
|
|
cmd.func = function(name, param, ...)
|
|
local split = param:split(" ")
|
|
local hasall
|
|
for i = 1 + skip, #split do
|
|
hasall = hasall or split[i] == "all"
|
|
end
|
|
if not hasall then return oldfunc(name, param, ...) end
|
|
local oldset = minetest.set_player_privs
|
|
local function helper(...)
|
|
minetest.set_player_privs = oldset
|
|
return ...
|
|
end
|
|
minetest.set_player_privs = function(gname, privs, ...)
|
|
local oldprivs = minetest.get_player_privs(gname)
|
|
if not oldprivs[ignorepriv] then
|
|
minetest.log("warning", string_format("%q priv excluded from "
|
|
.. "\"/%s %s\" command", ignorepriv, key, param))
|
|
privs[ignorepriv] = nil
|
|
end
|
|
return oldset(gname, privs, ...)
|
|
end
|
|
return helper(oldfunc(name, param, ...))
|
|
end
|
|
end
|
|
grantwrap("grant", 1)
|
|
grantwrap("grantme", 0)
|
|
|
|
-- Only those players who are not ignored for restarts.
|
|
local function non_ignored_players()
|
|
local t = {}
|
|
for _, p in ipairs(minetest.get_connected_players()) do
|
|
if not minetest.check_player_privs(p, ignorepriv) then
|
|
t[#t + 1] = p
|
|
end
|
|
end
|
|
return t
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Trigger conditions
|
|
|
|
do
|
|
local function restarttrigger(reason, time)
|
|
req = uptime() + (time or grace)
|
|
if not time then
|
|
if req < minuptime then req = minuptime
|
|
elseif req > maxuptime then req = maxuptime end
|
|
end
|
|
minetest.log("RESTART REQUEST (" .. reason .. ") IN " .. remaintext())
|
|
end
|
|
|
|
-- Server admins can manually request a restart, including with a
|
|
-- custom grace time. Restarts cannot be canceled entirely (nor
|
|
-- should they be, probably), but can be delayed indefinitely.
|
|
minetest.register_chatcommand("trigger_restart", {
|
|
description = "Signal a restart request manually, or reset countdown",
|
|
privs = {server = true},
|
|
func = function(name, param)
|
|
restarttrigger("manual by " .. name, tonumber(param))
|
|
end
|
|
})
|
|
|
|
-- Track whether we should shutdown the server when it becomes empty
|
|
local shutdown_on_empty
|
|
|
|
-- Automatically detect a restart condition.
|
|
local function restartcheck()
|
|
if req then return end
|
|
|
|
-- Trigger restart if the server has reached its maximum uptime.
|
|
if uptime() >= maxuptime - grace then
|
|
return restarttrigger("max uptime")
|
|
end
|
|
|
|
-- Trigger a restart if a file exists in the world dir to allow
|
|
-- an external script to request it.
|
|
local f = io_open(minetest.get_worldpath() .. "/restart")
|
|
if f then
|
|
f:close()
|
|
return restarttrigger("file trigger")
|
|
end
|
|
|
|
-- Trigger restart on the "always" condition.
|
|
if always then
|
|
if #non_ignored_players() > 0 then
|
|
shutdown_on_empty = true
|
|
elseif shutdown_on_empty then
|
|
return restarttrigger("server empty", 0)
|
|
end
|
|
end
|
|
|
|
return minetest.after(2, restartcheck)
|
|
end
|
|
minetest.after(0, restartcheck)
|
|
|
|
-- Restart check for "always" condition immediately.
|
|
if always then
|
|
minetest.register_on_joinplayer(function()
|
|
minetest.after(0, restartcheck)
|
|
end)
|
|
minetest.register_on_leaveplayer(function()
|
|
minetest.after(0, restartcheck)
|
|
end)
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Announce pending restarts in chat streams
|
|
|
|
local announced
|
|
do
|
|
local lastsent
|
|
minetest.register_globalstep(function()
|
|
-- Skip if no countdown yet.
|
|
if not req then return end
|
|
|
|
-- Never announce if no players online.
|
|
if #non_ignored_players() < 1 then
|
|
lastsent = nil
|
|
return
|
|
end
|
|
|
|
-- Don't bother chat with announcements too far in advance.
|
|
if remain() > announcetime then return end
|
|
|
|
-- Announce if the remaining time has changed, or
|
|
-- we've crossed to/from the active countdown phase.
|
|
if (announced ~= req) or (not lastsent)
|
|
or ((lastsent > countdown) ~= (remain() > countdown)) then
|
|
announced = req
|
|
lastsent = remain()
|
|
minetest.chat_send_all(string_format(chatmsg, remaintext()))
|
|
end
|
|
end)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Handle actual restart event
|
|
|
|
do
|
|
local shuttingdown
|
|
minetest.register_globalstep(function()
|
|
if shuttingdown or not req then return end
|
|
local pcount = #non_ignored_players()
|
|
if pcount > 0 and remain() > 0 then return end
|
|
shuttingdown = true
|
|
if pcount > 0 then
|
|
minetest.chat_send_all(kickmsg)
|
|
modstore:set_string("announce", "1")
|
|
elseif announced then
|
|
minetest.chat_send_all(restartmsg)
|
|
modstore:set_string("announce", "1")
|
|
end
|
|
return minetest.request_shutdown(shtudownmsg, true)
|
|
end)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Announce restart complete after an announced shutdown
|
|
|
|
do
|
|
local skip = modstore:get_string("announce") == ""
|
|
local function announcestart()
|
|
if skip then return end
|
|
minetest.chat_send_all(completemsg)
|
|
skip = true
|
|
modstore:set_string("announce", "")
|
|
end
|
|
minetest.after(completedelay, announcestart)
|
|
minetest.register_on_joinplayer(announcestart)
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Add pending restarts to status line
|
|
|
|
do
|
|
local function modstatus(text, ...)
|
|
if not req then return text, ... end
|
|
local parts = text:split("|")
|
|
for i = 1, #parts do
|
|
if string_match(parts[i], "^%s*uptime:") then
|
|
table_insert(parts, i + 1, " restart in " .. remaintext() .. " ")
|
|
break
|
|
end
|
|
end
|
|
text = table_concat(parts, "|")
|
|
return text, ...
|
|
end
|
|
local oldstatus = minetest.get_server_status
|
|
function minetest.get_server_status(...)
|
|
return modstatus(oldstatus(...))
|
|
end
|
|
end
|
|
|
|
------------------------------------------------------------------------
|
|
-- Announce pending restarts via HUD
|
|
|
|
do
|
|
local huds = {}
|
|
local hud_elem_type = minetest.features.hud_def_type_field and "type" or "hud_elem_type"
|
|
minetest.register_on_leaveplayer(function(player)
|
|
huds[player:get_player_name()] = nil
|
|
end)
|
|
minetest.register_globalstep(function()
|
|
if #minetest.get_connected_players() < 1 then return end
|
|
|
|
if (not req) or (remain() > countdown) then
|
|
for pname, hud in pairs(huds) do
|
|
local player = minetest.get_player_by_name(pname)
|
|
if player then player:hud_remove(hud.id) end
|
|
huds[pname] = nil
|
|
end
|
|
return
|
|
end
|
|
|
|
local number = (remain() < critical and (remain() - math_floor(remain()) < 0.5))
|
|
and hudcolorflash or hudcolor
|
|
local text = string_format(hudmsg, remaintext())
|
|
|
|
for _, player in ipairs(minetest.get_connected_players()) do
|
|
local pname = player:get_player_name()
|
|
local hud = huds[pname]
|
|
if hud then
|
|
if hud.number ~= number then
|
|
player:hud_change(hud.id, "number", number)
|
|
hud.number = number
|
|
end
|
|
if hud.text ~= text then
|
|
player:hud_change(hud.id, "text", text)
|
|
hud.text = text
|
|
end
|
|
else
|
|
huds[pname] = {
|
|
number = number,
|
|
text = text,
|
|
id = player:hud_add({
|
|
label = "restart_warn",
|
|
[hud_elem_type] = "text",
|
|
position = {x = 0.5, y = 0.8},
|
|
text = text,
|
|
number = number,
|
|
alignment = {x = 0, y = 1},
|
|
offset = {x = 0, y = 0},
|
|
z_index = 100,
|
|
})
|
|
}
|
|
end
|
|
end
|
|
end)
|
|
end
|