2024-09-13 07:00:26 -04:00

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