Enable the server to request the client to reconnect.
This can be done with the now extended minetest.request_shutdown([reason], [reconnect]) setting.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
+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:")
--------------------------------------------------------------------------------
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)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
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
* 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
# 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
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()),
bool accessDenied()
{ return m_access_denied; }
+ bool reconnectRequested() { return m_access_denied_reconnect; }
+
std::string accessDeniedReason()
{ return m_access_denied_reason; }
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;
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;
*/
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)
receiver->m_touchscreengui = new TouchScreenGUI(device, receiver);
g_touchscreengui = receiver->m_touchscreengui;
#endif
+
the_game(
kill,
random_input,
current_port,
error_message,
chat_backend,
+ &reconnect_requested,
gamespec,
simple_singleplayer_mode
);
}
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();
}
}
- 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;
}
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);
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");
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);
}
}
#include "mapnode.h"
#include "mapblock.h"
#include "jthread/jmutex.h"
+#include "network/networkprotocol.h" // for AccessDeniedCode
class ServerEnvironment;
class ActiveBlockModifier;
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);
* 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();
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);
scene::ISceneManager *smgr;
bool *kill;
std::string *error_message;
+ bool *reconnect_requested;
IGameDef *gamedef; // Convenience (same as *client)
scene::ISceneNode *skybox;
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();
if (client->accessDenied()) {
*error_message = "Access denied. Reason: "
+ client->accessDeniedReason();
+ *reconnect_requested = client->reconnectRequested();
errorstream << *error_message << std::endl;
break;
}
if (client->accessDenied()) {
*error_message = "Access denied. Reason: "
+ client->accessDeniedReason();
+ *reconnect_requested = client->reconnectRequested();
errorstream << *error_message << std::endl;
return false;
}
std::string &error_message,
ChatBackend &chat_backend,
+ bool *reconnect_requested,
const SubgameSpec &gamespec, // Used for local game
bool simple_singleplayer_mode)
{
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();
u16 port,
std::string &error_message,
ChatBackend &chat_backend,
+ bool *reconnect_requested,
const SubgameSpec &gamespec, // Used for local game
bool simple_singleplayer_mode);
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;
}
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();
#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;
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)
{}
};
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
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,
SERVER_ACCESSDENIED_ALREADY_CONNECTED,
SERVER_ACCESSDENIED_SERVER_FAIL,
SERVER_ACCESSDENIED_CUSTOM_STRING,
+ SERVER_ACCESSDENIED_SHUTDOWN,
+ SERVER_ACCESSDENIED_CRASH,
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
hp(PLAYER_MAX_HP),
hurt_tilt_timer(0),
hurt_tilt_strength(0),
+ protocol_version(0),
peer_id(PEER_ID_INEXISTENT),
keyPressed(0),
// protected
float hurt_tilt_timer;
float hurt_tilt_strength;
+ u16 protocol_version;
u16 peer_id;
std::string inventory_formspec;
#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);
}
#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
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;
// 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;
}
class ModApiServer : public ModApiBase {
private:
- // request_shutdown()
+ // request_shutdown([message], [reconnect])
static int l_request_shutdown(lua_State *L);
// get_server_status()
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)
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();
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;
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();
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);
}
playersao->setPos(pos);
}
}
+
+
void Server::DenySudoAccess(u16 peer_id)
{
DSTACK(__FUNCTION_NAME);
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);
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;
getPlayerEffectivePrivs(player->getName()),
isSingleplayer());
+ player->protocol_version = proto_version;
+
/* Clean up old HUD elements from previous sessions */
player->clearHud();
{ 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
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);
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);
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();
*/
bool m_shutdown_requested;
+ std::string m_shutdown_msg;
+ bool m_shutdown_ask_reconnect;
/*
Map edit event queue. Automatically receives all map edits.