-- 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() ------------------------------------------------------------------------ -- 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" -- 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) elseif announced then minetest.chat_send_all(restartmsg) end return minetest.request_shutdown(shtudownmsg, true) end) 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 = {} 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} }) } end end end) end