From 3b50b2766aeb09c9fc0ad0ea07426bb2187df3d7 Mon Sep 17 00:00:00 2001
From: est31 <MTest31@outlook.com>
Date: Fri, 17 Jul 2015 16:40:41 +0200
Subject: [PATCH] Optional reconnect functionality

Enable the server to request the client to reconnect.

This can be done with the now extended minetest.request_shutdown([reason], [reconnect]) setting.
---
 builtin/fstk/ui.lua                 | 65 +++++++++++++++++------------
 doc/lua_api.txt                     |  3 +-
 minetest.conf.example               |  3 ++
 src/client.cpp                      |  1 +
 src/client.h                        |  3 ++
 src/client/clientlauncher.cpp       | 24 +++++++----
 src/client/clientlauncher.h         |  4 +-
 src/defaultsettings.cpp             |  1 +
 src/environment.cpp                 |  8 ++--
 src/environment.h                   |  4 +-
 src/game.cpp                        | 26 +++++++-----
 src/game.h                          |  1 +
 src/guiEngine.cpp                   | 11 ++---
 src/guiMainMenu.h                   | 31 ++++++++------
 src/network/clientpackethandler.cpp | 23 ++++++++--
 src/network/networkprotocol.h       |  9 +++-
 src/player.cpp                      |  1 +
 src/player.h                        |  1 +
 src/script/cpp_api/s_mainmenu.cpp   | 10 ++++-
 src/script/cpp_api/s_mainmenu.h     | 11 +++--
 src/script/lua_api/l_mainmenu.cpp   | 22 ++++++----
 src/script/lua_api/l_server.cpp     |  4 +-
 src/script/lua_api/l_server.h       |  2 +-
 src/server.cpp                      | 57 ++++++++++++++++++++-----
 src/server.h                        | 18 ++++++--
 25 files changed, 233 insertions(+), 110 deletions(-)

diff --git a/builtin/fstk/ui.lua b/builtin/fstk/ui.lua
index 478a78ad5..de8ae4d2c 100644
--- a/builtin/fstk/ui.lua
+++ b/builtin/fstk/ui.lua
@@ -54,29 +54,42 @@ end
 --------------------------------------------------------------------------------
 --------------------------------------------------------------------------------
 
+local function wordwrap_quickhack(str)
+	local res = ""
+	local ar = str:split("\n")
+	for i = 1, #ar do
+		local text = ar[i]
+		-- Hack to add word wrapping.
+		-- TODO: Add engine support for wrapping in formspecs
+		while #text > 80 do
+			if res ~= "" then
+				res = res .. ","
+			end
+			res = res .. core.formspec_escape(string.sub(text, 1, 79))
+			text = string.sub(text, 80, #text)
+		end
+		if res ~= "" then
+			res = res .. ","
+		end
+		res = res .. core.formspec_escape(text)
+	end
+	return res
+end
+
 --------------------------------------------------------------------------------
 function ui.update()
 	local formspec = ""
 
 	-- handle errors
-	if gamedata ~= nil and gamedata.errormessage ~= nil then
-		local ar = gamedata.errormessage:split("\n")
-		for i = 1, #ar do
-			local text = ar[i]
-			-- Hack to add word wrapping.
-			-- TODO: Add engine support for wrapping in formspecs
-			while #text > 80 do
-				if formspec ~= "" then
-					formspec = formspec .. ","
-				end
-				formspec = formspec .. core.formspec_escape(string.sub(text, 1, 79))
-				text = string.sub(text, 80, #text)
-			end
-			if formspec ~= "" then
-				formspec = formspec .. ","
-			end
-			formspec = formspec .. core.formspec_escape(text)
-		end
+	if gamedata ~= nil and gamedata.reconnect_requested then
+		formspec = wordwrap_quickhack(gamedata.errormessage or "")
+		formspec = "size[12,5]" ..
+				"label[0.5,0;" .. fgettext("The server has requested a reconnect:") ..
+				"]textlist[0.2,0.8;11.5,3.5;;" .. formspec ..
+				"]button[6,4.6;3,0.5;btn_reconnect_no;" .. fgettext("Main menu") .. "]" ..
+				"button[3,4.6;3,0.5;btn_reconnect_yes;" .. fgettext("Reconnect") .. "]"
+	elseif gamedata ~= nil and gamedata.errormessage ~= nil then
+		formspec = wordwrap_quickhack(gamedata.errormessage)
 		local error_title
 		if string.find(gamedata.errormessage, "ModError") then
 			error_title = fgettext("An error occured in a Lua script, such as a mod:")
@@ -128,13 +141,6 @@ end
 
 --------------------------------------------------------------------------------
 function ui.handle_buttons(fields)
-
-	if fields["btn_error_confirm"] then
-		gamedata.errormessage = nil
-		update_menu()
-		return
-	end
-
 	for key,value in pairs(ui.childlist) do
 
 		local retval = value:handle_buttons(fields)
@@ -168,8 +174,15 @@ end
 --------------------------------------------------------------------------------
 --------------------------------------------------------------------------------
 core.button_handler = function(fields)
-	if fields["btn_error_confirm"] then
+	if fields["btn_reconnect_yes"] then
+		gamedata.reconnect_requested = false
+		gamedata.errormessage = nil
+		gamedata.do_reconnect = true
+		core.start()
+		return
+	elseif fields["btn_reconnect_no"] or fields["btn_error_confirm"] then
 		gamedata.errormessage = nil
+		gamedata.reconnect_requested = false
 		ui.update()
 		return
 	end
diff --git a/doc/lua_api.txt b/doc/lua_api.txt
index b11acb921..75bbbdb07 100644
--- a/doc/lua_api.txt
+++ b/doc/lua_api.txt
@@ -2174,7 +2174,8 @@ These functions return the leftover itemstack.
     * Optional: Variable number of arguments that are passed to `func`
 
 ### Server
-* `minetest.request_shutdown()`: request for server shutdown
+* `minetest.request_shutdown([message],[reconnect])`: request for server shutdown. Will display `message` to clients,
+    and `reconnect` == true displays a reconnect button.
 * `minetest.get_server_status()`: returns server status string
 
 ### Bans
diff --git a/minetest.conf.example b/minetest.conf.example
index 51660e333..b9e7d59e4 100644
--- a/minetest.conf.example
+++ b/minetest.conf.example
@@ -393,6 +393,9 @@
 #    A message to be displayed to all clients when the server shuts down
 #kick_msg_crash = This server has experienced an internal error. You will now be disconnected.
 #    A message to be displayed to all clients when the server crashes
+#ask_reconnect_on_crash = false
+#    Whether to ask clients to reconnect after a (lua) crash.
+#    Set this to true if your server is set up to restart automatically.
 
 #    Mod profiler
 #mod_profiling = false
diff --git a/src/client.cpp b/src/client.cpp
index 1bd8c39ae..74072d9d7 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -244,6 +244,7 @@ Client::Client(
 	m_chosen_auth_mech(AUTH_MECHANISM_NONE),
 	m_auth_data(NULL),
 	m_access_denied(false),
+	m_access_denied_reconnect(false),
 	m_itemdef_received(false),
 	m_nodedef_received(false),
 	m_media_downloader(new ClientMediaDownloader()),
diff --git a/src/client.h b/src/client.h
index 7c1a19eed..547edfeab 100644
--- a/src/client.h
+++ b/src/client.h
@@ -489,6 +489,8 @@ class Client : public con::PeerHandler, public InventoryManager, public IGameDef
 	bool accessDenied()
 	{ return m_access_denied; }
 
+	bool reconnectRequested() { return m_access_denied_reconnect; }
+
 	std::string accessDeniedReason()
 	{ return m_access_denied_reason; }
 
@@ -636,6 +638,7 @@ class Client : public con::PeerHandler, public InventoryManager, public IGameDef
 
 
 	bool m_access_denied;
+	bool m_access_denied_reconnect;
 	std::string m_access_denied_reason;
 	std::queue<ClientEvent> m_client_event_queue;
 	bool m_itemdef_received;
diff --git a/src/client/clientlauncher.cpp b/src/client/clientlauncher.cpp
index 50f0690d8..bad5c384c 100644
--- a/src/client/clientlauncher.cpp
+++ b/src/client/clientlauncher.cpp
@@ -168,8 +168,9 @@ bool ClientLauncher::run(GameParams &game_params, const Settings &cmd_args)
 	ChatBackend chat_backend;
 
 	// If an error occurs, this is set to something by menu().
-	// It is then displayed before	the menu shows on the next call to menu()
+	// It is then displayed before the menu shows on the next call to menu()
 	std::string error_message;
+	bool reconnect_requested = false;
 
 	bool first_loop = true;
 
@@ -197,7 +198,8 @@ bool ClientLauncher::run(GameParams &game_params, const Settings &cmd_args)
 			*/
 			guiroot = guienv->addStaticText(L"", core::rect<s32>(0, 0, 10000, 10000));
 
-			bool game_has_run = launch_game(error_message, game_params, cmd_args);
+			bool game_has_run = launch_game(error_message, reconnect_requested,
+				game_params, cmd_args);
 
 			// If skip_main_menu, we only want to startup once
 			if (skip_main_menu && !first_loop)
@@ -233,6 +235,7 @@ bool ClientLauncher::run(GameParams &game_params, const Settings &cmd_args)
 			receiver->m_touchscreengui = new TouchScreenGUI(device, receiver);
 			g_touchscreengui = receiver->m_touchscreengui;
 #endif
+
 			the_game(
 				kill,
 				random_input,
@@ -245,6 +248,7 @@ bool ClientLauncher::run(GameParams &game_params, const Settings &cmd_args)
 				current_port,
 				error_message,
 				chat_backend,
+				&reconnect_requested,
 				gamespec,
 				simple_singleplayer_mode
 			);
@@ -325,14 +329,16 @@ bool ClientLauncher::init_engine(int log_level)
 }
 
 bool ClientLauncher::launch_game(std::string &error_message,
-		GameParams &game_params, const Settings &cmd_args)
+		bool reconnect_requested, GameParams &game_params,
+		const Settings &cmd_args)
 {
 	// Initialize menu data
 	MainMenuData menudata;
-	menudata.address      = address;
-	menudata.name         = playername;
-	menudata.port         = itos(game_params.socket_port);
-	menudata.errormessage = error_message;
+	menudata.address                         = address;
+	menudata.name                            = playername;
+	menudata.port                            = itos(game_params.socket_port);
+	menudata.script_data.errormessage        = error_message;
+	menudata.script_data.reconnect_requested = reconnect_requested;
 
 	error_message.clear();
 
@@ -379,11 +385,11 @@ bool ClientLauncher::launch_game(std::string &error_message,
 		}
 	}
 
-	if (!menudata.errormessage.empty()) {
+	if (!menudata.script_data.errormessage.empty()) {
 		/* The calling function will pass this back into this function upon the
 		 * next iteration (if any) causing it to be displayed by the GUI
 		 */
-		error_message = menudata.errormessage;
+		error_message = menudata.script_data.errormessage;
 		return false;
 	}
 
diff --git a/src/client/clientlauncher.h b/src/client/clientlauncher.h
index 09f8c039a..49ceefc52 100644
--- a/src/client/clientlauncher.h
+++ b/src/client/clientlauncher.h
@@ -92,8 +92,8 @@ class ClientLauncher
 	void init_args(GameParams &game_params, const Settings &cmd_args);
 	bool init_engine(int log_level);
 
-	bool launch_game(std::string &error_message, GameParams &game_params,
-			const Settings &cmd_args);
+	bool launch_game(std::string &error_message, bool reconnect_requested,
+		GameParams &game_params, const Settings &cmd_args);
 
 	void main_menu(MainMenuData *menudata);
 	bool create_engine_device(int log_level);
diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp
index 8df0fbbd8..42c617513 100644
--- a/src/defaultsettings.cpp
+++ b/src/defaultsettings.cpp
@@ -254,6 +254,7 @@ void set_default_settings(Settings *settings)
 
 	settings->setDefault("kick_msg_shutdown", "Server shutting down.");
 	settings->setDefault("kick_msg_crash", "This server has experienced an internal error. You will now be disconnected.");
+	settings->setDefault("ask_reconnect_on_crash", "false");
 
 	settings->setDefault("profiler_print_interval", "0");
 	settings->setDefault("enable_mapgen_debug_info", "false");
diff --git a/src/environment.cpp b/src/environment.cpp
index d22982b68..5d7321f60 100644
--- a/src/environment.cpp
+++ b/src/environment.cpp
@@ -426,13 +426,15 @@ bool ServerEnvironment::line_of_sight(v3f pos1, v3f pos2, float stepsize, v3s16
 	return true;
 }
 
-void ServerEnvironment::kickAllPlayers(const std::string &reason)
+void ServerEnvironment::kickAllPlayers(AccessDeniedCode reason,
+		const std::string &str_reason, bool reconnect)
 {
-	std::wstring wreason = utf8_to_wide(reason);
 	for (std::vector<Player*>::iterator it = m_players.begin();
 			it != m_players.end();
 			++it) {
-		((Server*)m_gamedef)->DenyAccess_Legacy((*it)->peer_id, wreason);
+		((Server*)m_gamedef)->DenyAccessVerCompliant((*it)->peer_id,
+			(*it)->protocol_version, (AccessDeniedCode)reason,
+			str_reason, reconnect);
 	}
 }
 
diff --git a/src/environment.h b/src/environment.h
index 794f1971c..2bc128f40 100644
--- a/src/environment.h
+++ b/src/environment.h
@@ -40,6 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "mapnode.h"
 #include "mapblock.h"
 #include "jthread/jmutex.h"
+#include "network/networkprotocol.h" // for AccessDeniedCode
 
 class ServerEnvironment;
 class ActiveBlockModifier;
@@ -221,7 +222,8 @@ class ServerEnvironment : public Environment
 	float getSendRecommendedInterval()
 		{ return m_recommended_send_interval; }
 
-	void kickAllPlayers(const std::string &reason);
+	void kickAllPlayers(AccessDeniedCode reason,
+		const std::string &str_reason, bool reconnect);
 	// Save players
 	void saveLoadedPlayers();
 	void savePlayer(const std::string &playername);
diff --git a/src/game.cpp b/src/game.cpp
index e3d6b0bc6..09b8aab42 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -1417,8 +1417,7 @@ struct VolatileRunFlags {
  * hides most of the stuff in this class (nothing in this class is required
  * by any other file) but exposes the public methods/data only.
  */
-class Game
-{
+class Game {
 public:
 	Game();
 	~Game();
@@ -1434,6 +1433,7 @@ class Game
 			std::string *address,
 			u16 port,
 			std::string &error_message,
+			bool *reconnect,
 			ChatBackend *chat_backend,
 			const SubgameSpec &gamespec,    // Used for local game
 			bool simple_singleplayer_mode);
@@ -1588,6 +1588,7 @@ class Game
 	scene::ISceneManager *smgr;
 	bool *kill;
 	std::string *error_message;
+	bool *reconnect_requested;
 	IGameDef *gamedef;                     // Convenience (same as *client)
 	scene::ISceneNode *skybox;
 
@@ -1716,17 +1717,19 @@ bool Game::startup(bool *kill,
 		std::string *address,     // can change if simple_singleplayer_mode
 		u16 port,
 		std::string &error_message,
+		bool *reconnect,
 		ChatBackend *chat_backend,
 		const SubgameSpec &gamespec,
 		bool simple_singleplayer_mode)
 {
 	// "cache"
-	this->device        = device;
-	this->kill          = kill;
-	this->error_message = &error_message;
-	this->random_input  = random_input;
-	this->input         = input;
-	this->chat_backend  = chat_backend;
+	this->device              = device;
+	this->kill                = kill;
+	this->error_message       = &error_message;
+	this->reconnect_requested = reconnect;
+	this->random_input        = random_input;
+	this->input               = input;
+	this->chat_backend        = chat_backend;
 	this->simple_singleplayer_mode = simple_singleplayer_mode;
 
 	driver              = device->getVideoDriver();
@@ -2239,6 +2242,7 @@ bool Game::connectToServer(const std::string &playername,
 			if (client->accessDenied()) {
 				*error_message = "Access denied. Reason: "
 						+ client->accessDeniedReason();
+				*reconnect_requested = client->reconnectRequested();
 				errorstream << *error_message << std::endl;
 				break;
 			}
@@ -2376,6 +2380,7 @@ inline bool Game::checkConnection()
 	if (client->accessDenied()) {
 		*error_message = "Access denied. Reason: "
 				+ client->accessDeniedReason();
+		*reconnect_requested = client->reconnectRequested();
 		errorstream << *error_message << std::endl;
 		return false;
 	}
@@ -4330,6 +4335,7 @@ void the_game(bool *kill,
 
 		std::string &error_message,
 		ChatBackend &chat_backend,
+		bool *reconnect_requested,
 		const SubgameSpec &gamespec,        // Used for local game
 		bool simple_singleplayer_mode)
 {
@@ -4344,8 +4350,8 @@ void the_game(bool *kill,
 	try {
 
 		if (game.startup(kill, random_input, input, device, map_dir,
-				playername, password, &server_address, port,
-				error_message, &chat_backend, gamespec,
+				playername, password, &server_address, port, error_message,
+				reconnect_requested, &chat_backend, gamespec,
 				simple_singleplayer_mode)) {
 			game.run();
 			game.shutdown();
diff --git a/src/game.h b/src/game.h
index 358b26c37..e1f4e9346 100644
--- a/src/game.h
+++ b/src/game.h
@@ -147,6 +147,7 @@ void the_game(bool *kill,
 		u16 port,
 		std::string &error_message,
 		ChatBackend &chat_backend,
+		bool *reconnect_requested,
 		const SubgameSpec &gamespec, // Used for local game
 		bool simple_singleplayer_mode);
 
diff --git a/src/guiEngine.cpp b/src/guiEngine.cpp
index eac9db0c6..c616bc322 100644
--- a/src/guiEngine.cpp
+++ b/src/guiEngine.cpp
@@ -208,10 +208,8 @@ GUIEngine::GUIEngine(	irr::IrrlichtDevice* dev,
 	m_script = new MainMenuScripting(this);
 
 	try {
-		if (m_data->errormessage != "") {
-			m_script->setMainMenuErrorMessage(m_data->errormessage);
-			m_data->errormessage = "";
-		}
+		m_script->setMainMenuData(&m_data->script_data);
+		m_data->script_data.errormessage = "";
 
 		if (!loadMainMenuScript()) {
 			errorstream << "No future without mainmenu" << std::endl;
@@ -219,10 +217,9 @@ GUIEngine::GUIEngine(	irr::IrrlichtDevice* dev,
 		}
 
 		run();
-	}
-	catch(LuaError &e) {
+	} catch (LuaError &e) {
 		errorstream << "MAINMENU ERROR: " << e.what() << std::endl;
-		m_data->errormessage = e.what();
+		m_data->script_data.errormessage = e.what();
 	}
 
 	m_menu->quitMenu();
diff --git a/src/guiMainMenu.h b/src/guiMainMenu.h
index 34362dba6..711ad10f8 100644
--- a/src/guiMainMenu.h
+++ b/src/guiMainMenu.h
@@ -25,17 +25,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <string>
 #include <list>
 
-enum
-{
-	TAB_SINGLEPLAYER=0,
-	TAB_MULTIPLAYER,
-	TAB_ADVANCED,
-	TAB_SETTINGS,
-	TAB_CREDITS
+struct MainMenuDataForScript {
+
+	MainMenuDataForScript() :
+		reconnect_requested(false)
+	{}
+
+	// Whether the server has requested a reconnect
+	bool reconnect_requested;
+
+	std::string errormessage;
 };
 
-struct MainMenuData
-{
+struct MainMenuData {
 	// Client options
 	std::string servername;
 	std::string serverdescription;
@@ -43,19 +45,22 @@ struct MainMenuData
 	std::string port;
 	std::string name;
 	std::string password;
+	// Whether to reconnect
+	bool do_reconnect;
 
 	// Server options
 	bool enable_public;
 	int selected_world;
 	bool simple_singleplayer_mode;
 
-	//error handling
-	std::string errormessage;
+	// Data to be passed to the script
+	MainMenuDataForScript script_data;
+
 	MainMenuData():
+		do_reconnect(false),
 		enable_public(false),
 		selected_world(0),
-		simple_singleplayer_mode(false),
-		errormessage("")
+		simple_singleplayer_mode(false)
 	{}
 };
 
diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp
index 15d5456fa..2133543d9 100644
--- a/src/network/clientpackethandler.cpp
+++ b/src/network/clientpackethandler.cpp
@@ -215,11 +215,28 @@ void Client::handleCommand_AccessDenied(NetworkPacket* pkt)
 
 		u8 denyCode = SERVER_ACCESSDENIED_UNEXPECTED_DATA;
 		*pkt >> denyCode;
-		if (denyCode == SERVER_ACCESSDENIED_CUSTOM_STRING) {
+		if (denyCode == SERVER_ACCESSDENIED_SHUTDOWN ||
+				denyCode == SERVER_ACCESSDENIED_CRASH) {
 			*pkt >> m_access_denied_reason;
-		}
-		else if (denyCode < SERVER_ACCESSDENIED_MAX) {
+			if (m_access_denied_reason == "") {
+				m_access_denied_reason = accessDeniedStrings[denyCode];
+			}
+			u8 reconnect;
+			*pkt >> reconnect;
+			m_access_denied_reconnect = reconnect & 1;
+		} else if (denyCode == SERVER_ACCESSDENIED_CUSTOM_STRING) {
+			*pkt >> m_access_denied_reason;
+		} else if (denyCode < SERVER_ACCESSDENIED_MAX) {
 			m_access_denied_reason = accessDeniedStrings[denyCode];
+		} else {
+			// Allow us to add new error messages to the
+			// protocol without raising the protocol version, if we want to.
+			// Until then (which may be never), this is outside
+			// of the defined protocol.
+			*pkt >> m_access_denied_reason;
+			if (m_access_denied_reason == "") {
+				m_access_denied_reason = "Unknown";
+			}
 		}
 	}
 	// 13/03/15 Legacy code from 0.4.12 and lesser. must stay 1 year
diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h
index e4b566740..feb18e509 100644
--- a/src/network/networkprotocol.h
+++ b/src/network/networkprotocol.h
@@ -202,7 +202,8 @@ enum ToClientCommand
 	TOCLIENT_ACCESS_DENIED = 0x0A,
 	/*
 		u8 reason
-		std::string custom reason (if reason == SERVER_ACCESSDENIED_CUSTOM_STRING)
+		std::string custom reason (if needed, otherwise "")
+		u8 (bool) reconnect
 	*/
 	TOCLIENT_BLOCKDATA = 0x20, //TODO: Multiple blocks
 	TOCLIENT_ADDNODE = 0x21,
@@ -937,6 +938,8 @@ enum AccessDeniedCode {
 	SERVER_ACCESSDENIED_ALREADY_CONNECTED,
 	SERVER_ACCESSDENIED_SERVER_FAIL,
 	SERVER_ACCESSDENIED_CUSTOM_STRING,
+	SERVER_ACCESSDENIED_SHUTDOWN,
+	SERVER_ACCESSDENIED_CRASH,
 	SERVER_ACCESSDENIED_MAX,
 };
 
@@ -954,8 +957,10 @@ const static std::string accessDeniedStrings[SERVER_ACCESSDENIED_MAX] = {
 	"Too many users.",
 	"Empty passwords are disallowed.  Set a password and try again.",
 	"Another client is connected with this name.  If your client closed unexpectedly, try again in a minute.",
-	"Server authention failed.  This is likely a server error."
+	"Server authentication failed.  This is likely a server error.",
 	"",
+	"Server shutting down.",
+	"This server has experienced an internal error. You will now be disconnected."
 };
 
 #endif
diff --git a/src/player.cpp b/src/player.cpp
index 668d490cc..0e8fd86d2 100644
--- a/src/player.cpp
+++ b/src/player.cpp
@@ -44,6 +44,7 @@ Player::Player(IGameDef *gamedef, const char *name):
 	hp(PLAYER_MAX_HP),
 	hurt_tilt_timer(0),
 	hurt_tilt_strength(0),
+	protocol_version(0),
 	peer_id(PEER_ID_INEXISTENT),
 	keyPressed(0),
 // protected
diff --git a/src/player.h b/src/player.h
index c84cc1c92..3a336afc4 100644
--- a/src/player.h
+++ b/src/player.h
@@ -362,6 +362,7 @@ class Player
 	float hurt_tilt_timer;
 	float hurt_tilt_strength;
 
+	u16 protocol_version;
 	u16 peer_id;
 
 	std::string inventory_formspec;
diff --git a/src/script/cpp_api/s_mainmenu.cpp b/src/script/cpp_api/s_mainmenu.cpp
index 0bb247fa0..7430b0f4f 100644
--- a/src/script/cpp_api/s_mainmenu.cpp
+++ b/src/script/cpp_api/s_mainmenu.cpp
@@ -21,15 +21,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "cpp_api/s_internal.h"
 #include "common/c_converter.h"
 
-void ScriptApiMainMenu::setMainMenuErrorMessage(std::string errormessage)
+void ScriptApiMainMenu::setMainMenuData(MainMenuDataForScript *data)
 {
 	SCRIPTAPI_PRECHECKHEADER
 
 	lua_getglobal(L, "gamedata");
 	int gamedata_idx = lua_gettop(L);
 	lua_pushstring(L, "errormessage");
-	lua_pushstring(L, errormessage.c_str());
+	if (!data->errormessage.empty()) {
+		lua_pushstring(L, data->errormessage.c_str());
+	} else {
+		lua_pushnil(L);
+	}
 	lua_settable(L, gamedata_idx);
+	setboolfield(L, gamedata_idx, "reconnect_requested",
+		data->reconnect_requested);
 	lua_pop(L, 1);
 }
 
diff --git a/src/script/cpp_api/s_mainmenu.h b/src/script/cpp_api/s_mainmenu.h
index 6994b578b..8d5895817 100644
--- a/src/script/cpp_api/s_mainmenu.h
+++ b/src/script/cpp_api/s_mainmenu.h
@@ -22,16 +22,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #include "cpp_api/s_base.h"
 #include "util/string.h"
+#include "../guiMainMenu.h"
 
-class ScriptApiMainMenu
-		: virtual public ScriptApiBase
-{
+class ScriptApiMainMenu : virtual public ScriptApiBase {
 public:
 	/**
-	 * set gamedata.errormessage to inform lua of an error
-	 * @param errormessage the error message
+	 * Hand over MainMenuDataForScript to lua to inform lua of the content
+	 * @param data the data
 	 */
-	void setMainMenuErrorMessage(std::string errormessage);
+	void setMainMenuData(MainMenuDataForScript *data);
 
 	/**
 	 * process events received from formspec
diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp
index d209582e9..92311d6fc 100644
--- a/src/script/lua_api/l_mainmenu.cpp
+++ b/src/script/lua_api/l_mainmenu.cpp
@@ -114,15 +114,19 @@ int ModApiMainMenu::l_start(lua_State *L)
 
 	bool valid = false;
 
-
-	engine->m_data->selected_world		= getIntegerData(L, "selected_world",valid) -1;
-	engine->m_data->simple_singleplayer_mode = getBoolData(L,"singleplayer",valid);
-	engine->m_data->name				= getTextData(L,"playername");
-	engine->m_data->password			= getTextData(L,"password");
-	engine->m_data->address				= getTextData(L,"address");
-	engine->m_data->port				= getTextData(L,"port");
-	engine->m_data->serverdescription	= getTextData(L,"serverdescription");
-	engine->m_data->servername			= getTextData(L,"servername");
+	MainMenuData *data = engine->m_data;
+
+	data->selected_world = getIntegerData(L, "selected_world",valid) -1;
+	data->simple_singleplayer_mode = getBoolData(L,"singleplayer",valid);
+	data->do_reconnect = getBoolData(L, "do_reconnect", valid);
+	if (!data->do_reconnect) {
+		data->name     = getTextData(L,"playername");
+		data->password = getTextData(L,"password");
+		data->address  = getTextData(L,"address");
+		data->port     = getTextData(L,"port");
+	}
+	data->serverdescription = getTextData(L,"serverdescription");
+	data->servername        = getTextData(L,"servername");
 
 	//close menu next time
 	engine->m_startgame = true;
diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp
index 558cc0885..96c0327df 100644
--- a/src/script/lua_api/l_server.cpp
+++ b/src/script/lua_api/l_server.cpp
@@ -30,7 +30,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 // request_shutdown()
 int ModApiServer::l_request_shutdown(lua_State *L)
 {
-	getServer(L)->requestShutdown();
+	const char *msg = lua_tolstring(L, 1, NULL);
+	bool reconnect = lua_toboolean(L, 2);
+	getServer(L)->requestShutdown(msg ? msg : "", reconnect);
 	return 0;
 }
 
diff --git a/src/script/lua_api/l_server.h b/src/script/lua_api/l_server.h
index fd85a8975..e14bef043 100644
--- a/src/script/lua_api/l_server.h
+++ b/src/script/lua_api/l_server.h
@@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 class ModApiServer : public ModApiBase {
 private:
-	// request_shutdown()
+	// request_shutdown([message], [reconnect])
 	static int l_request_shutdown(lua_State *L);
 
 	// get_server_status()
diff --git a/src/server.cpp b/src/server.cpp
index 2d6de1c9d..cb7e35ecd 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -191,6 +191,7 @@ Server::Server(
 	m_uptime(0),
 	m_clients(&m_con),
 	m_shutdown_requested(false),
+	m_shutdown_ask_reconnect(false),
 	m_ignore_map_edit_events(false),
 	m_ignore_map_edit_events_peer_id(0),
 	m_next_sound_id(0)
@@ -398,7 +399,17 @@ Server::~Server()
 		m_env->saveLoadedPlayers();
 
 		infostream << "Server: Kicking players" << std::endl;
-		m_env->kickAllPlayers(g_settings->get("kick_msg_shutdown"));
+		std::string kick_msg;
+		bool reconnect = false;
+		if (getShutdownRequested()) {
+			reconnect = m_shutdown_ask_reconnect;
+			kick_msg = m_shutdown_msg;
+		}
+		if (kick_msg == "") {
+			kick_msg = g_settings->get("kick_msg_shutdown");
+		}
+		m_env->kickAllPlayers(SERVER_ACCESSDENIED_SHUTDOWN,
+			kick_msg, reconnect);
 
 		infostream << "Server: Saving environment metadata" << std::endl;
 		m_env->saveMeta();
@@ -502,7 +513,9 @@ void Server::step(float dtime)
 			throw ServerError(async_err);
 		}
 		else {
-			m_env->kickAllPlayers(g_settings->get("kick_msg_crash"));
+			m_env->kickAllPlayers(SERVER_ACCESSDENIED_CRASH,
+				g_settings->get("kick_msg_crash"),
+				g_settings->getBool("ask_reconnect_on_crash"));
 			errorstream << "UNRECOVERABLE error occurred. Stopping server. "
 					<< "Please fix the following error:" << std::endl
 					<< async_err << std::endl;
@@ -1070,7 +1083,7 @@ PlayerSAO* Server::StageTwoClientInit(u16 peer_id)
 		RemoteClient* client = m_clients.lockedGetClientNoEx(peer_id, CS_InitDone);
 		if (client != NULL) {
 			playername = client->getName();
-			playersao = emergePlayer(playername.c_str(), peer_id);
+			playersao = emergePlayer(playername.c_str(), peer_id, client->net_proto_version);
 		}
 	} catch (std::exception &e) {
 		m_clients.Unlock();
@@ -1523,16 +1536,18 @@ void Server::SendBreath(u16 peer_id, u16 breath)
 	Send(&pkt);
 }
 
-void Server::SendAccessDenied(u16 peer_id, AccessDeniedCode reason, const std::string &custom_reason)
+void Server::SendAccessDenied(u16 peer_id, AccessDeniedCode reason,
+		const std::string &custom_reason, bool reconnect)
 {
-	DSTACK(__FUNCTION_NAME);
+	assert(reason < SERVER_ACCESSDENIED_MAX);
 
 	NetworkPacket pkt(TOCLIENT_ACCESS_DENIED, 1, peer_id);
-	pkt << (u8) reason;
-
-	if (reason == SERVER_ACCESSDENIED_CUSTOM_STRING) {
+	pkt << (u8)reason;
+	if (reason == SERVER_ACCESSDENIED_CUSTOM_STRING)
 		pkt << custom_reason;
-	}
+	else if (reason == SERVER_ACCESSDENIED_SHUTDOWN ||
+			reason == SERVER_ACCESSDENIED_CRASH)
+		pkt << custom_reason << (u8)reconnect;
 	Send(&pkt);
 }
 
@@ -2567,6 +2582,8 @@ void Server::RespawnPlayer(u16 peer_id)
 		playersao->setPos(pos);
 	}
 }
+
+
 void Server::DenySudoAccess(u16 peer_id)
 {
 	DSTACK(__FUNCTION_NAME);
@@ -2575,6 +2592,24 @@ void Server::DenySudoAccess(u16 peer_id)
 	Send(&pkt);
 }
 
+
+void Server::DenyAccessVerCompliant(u16 peer_id, u16 proto_ver, AccessDeniedCode reason,
+		const std::string &str_reason, bool reconnect)
+{
+	if (proto_ver >= 25) {
+		SendAccessDenied(peer_id, reason, str_reason);
+	} else {
+		std::wstring wreason = utf8_to_wide(
+			reason == SERVER_ACCESSDENIED_CUSTOM_STRING ? str_reason :
+			accessDeniedStrings[(u8)reason]);
+		SendAccessDenied_Legacy(peer_id, wreason);
+	}
+
+	m_clients.event(peer_id, CSE_SetDenied);
+	m_con.DisconnectPeer(peer_id);
+}
+
+
 void Server::DenyAccess(u16 peer_id, AccessDeniedCode reason, const std::string &custom_reason)
 {
 	DSTACK(__FUNCTION_NAME);
@@ -3330,7 +3365,7 @@ v3f Server::findSpawnPos()
 	return intToFloat(nodepos, BS);
 }
 
-PlayerSAO* Server::emergePlayer(const char *name, u16 peer_id)
+PlayerSAO* Server::emergePlayer(const char *name, u16 peer_id, u16 proto_version)
 {
 	bool newplayer = false;
 
@@ -3383,6 +3418,8 @@ PlayerSAO* Server::emergePlayer(const char *name, u16 peer_id)
 			getPlayerEffectivePrivs(player->getName()),
 			isSingleplayer());
 
+	player->protocol_version = proto_version;
+
 	/* Clean up old HUD elements from previous sessions */
 	player->clearHud();
 
diff --git a/src/server.h b/src/server.h
index fbca05f26..5f3905005 100644
--- a/src/server.h
+++ b/src/server.h
@@ -244,8 +244,13 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 			{ return m_shutdown_requested; }
 
 	// request server to shutdown
-	inline void requestShutdown(void)
-			{ m_shutdown_requested = true; }
+	inline void requestShutdown() { m_shutdown_requested = true; }
+	void requestShutdown(const std::string &msg, bool reconnect)
+	{
+		m_shutdown_requested = true;
+		m_shutdown_msg = msg;
+		m_shutdown_ask_reconnect = reconnect;
+	}
 
 	// Returns -1 if failed, sound handle on success
 	// Envlock
@@ -366,6 +371,8 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 	void deletingPeer(con::Peer *peer, bool timeout);
 
 	void DenySudoAccess(u16 peer_id);
+	void DenyAccessVerCompliant(u16 peer_id, u16 proto_ver, AccessDeniedCode reason,
+		const std::string &str_reason = "", bool reconnect = false);
 	void DenyAccess(u16 peer_id, AccessDeniedCode reason, const std::string &custom_reason="");
 	void acceptAuth(u16 peer_id, bool forSudoMode);
 	void DenyAccess_Legacy(u16 peer_id, const std::wstring &reason);
@@ -390,7 +397,8 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 	void SendMovement(u16 peer_id);
 	void SendHP(u16 peer_id, u8 hp);
 	void SendBreath(u16 peer_id, u16 breath);
-	void SendAccessDenied(u16 peer_id, AccessDeniedCode reason, const std::string &custom_reason);
+	void SendAccessDenied(u16 peer_id, AccessDeniedCode reason,
+		const std::string &custom_reason, bool reconnect = false);
 	void SendAccessDenied_Legacy(u16 peer_id, const std::wstring &reason);
 	void SendDeathscreen(u16 peer_id,bool set_camera_point_target, v3f camera_point_target);
 	void SendItemDef(u16 peer_id,IItemDefManager *itemdef, u16 protocol_version);
@@ -490,7 +498,7 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 
 		Call with env and con locked.
 	*/
-	PlayerSAO *emergePlayer(const char *name, u16 peer_id);
+	PlayerSAO *emergePlayer(const char *name, u16 peer_id, u16 proto_version);
 
 	void handlePeerChanges();
 
@@ -596,6 +604,8 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 	*/
 
 	bool m_shutdown_requested;
+	std::string m_shutdown_msg;
+	bool m_shutdown_ask_reconnect;
 
 	/*
 		Map edit event queue. Automatically receives all map edits.
-- 
GitLab