diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 23 | ||||
-rw-r--r-- | src/chat.h | 6 | ||||
-rw-r--r-- | src/chat_interface.h | 82 | ||||
-rw-r--r-- | src/cmake_config.h.in | 7 | ||||
-rw-r--r-- | src/debug.cpp | 12 | ||||
-rw-r--r-- | src/log.cpp | 16 | ||||
-rw-r--r-- | src/log.h | 7 | ||||
-rw-r--r-- | src/main.cpp | 97 | ||||
-rw-r--r-- | src/network/serverpackethandler.cpp | 67 | ||||
-rw-r--r-- | src/player.h | 1 | ||||
-rw-r--r-- | src/script/lua_api/l_server.cpp | 12 | ||||
-rw-r--r-- | src/script/lua_api/l_server.h | 3 | ||||
-rw-r--r-- | src/script/lua_api/l_util.h | 2 | ||||
-rw-r--r-- | src/server.cpp | 137 | ||||
-rw-r--r-- | src/server.h | 15 | ||||
-rw-r--r-- | src/terminal_chat_console.cpp | 452 | ||||
-rw-r--r-- | src/terminal_chat_console.h | 131 | ||||
-rw-r--r-- | src/unittest/test_utilities.cpp | 14 | ||||
-rw-r--r-- | src/util/string.h | 20 |
19 files changed, 1013 insertions, 91 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 72b52436c..55f5d4ad8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -160,6 +160,20 @@ find_package(Lua REQUIRED) find_package(GMP REQUIRED) +option(ENABLE_CURSES "Enable ncurses console" TRUE) +set(USE_CURSES FALSE) + +if(ENABLE_CURSES) + find_package(Ncursesw) + if(CURSES_FOUND) + set(USE_CURSES TRUE) + message(STATUS "ncurses console enabled.") + include_directories(${CURSES_INCLUDE_DIRS}) + else() + message(STATUS "ncurses not found!") + endif() +endif(ENABLE_CURSES) + option(ENABLE_LEVELDB "Enable LevelDB backend" TRUE) set(USE_LEVELDB FALSE) @@ -322,6 +336,7 @@ set(common_SRCS areastore.cpp ban.cpp cavegen.cpp + chat.cpp clientiface.cpp collision.cpp content_abm.cpp @@ -387,6 +402,7 @@ set(common_SRCS sound.cpp staticobject.cpp subgame.cpp + terminal_chat_console.cpp tool.cpp treegen.cpp version.cpp @@ -431,7 +447,6 @@ set(client_SRCS ${sound_SRCS} ${client_network_SRCS} camera.cpp - chat.cpp client.cpp clientmap.cpp clientmedia.cpp @@ -558,6 +573,9 @@ if(BUILD_CLIENT) ${CGUITTFONT_LIBRARY} ) endif() + if (USE_CURSES) + target_link_libraries(${PROJECT_NAME} ${CURSES_LIBRARIES}) + endif() if (USE_LEVELDB) target_link_libraries(${PROJECT_NAME} ${LEVELDB_LIBRARY}) endif() @@ -585,6 +603,9 @@ if(BUILD_SERVER) ) set_target_properties(${PROJECT_NAME}server PROPERTIES COMPILE_DEFINITIONS "SERVER") + if (USE_CURSES) + target_link_libraries(${PROJECT_NAME}server ${CURSES_LIBRARIES}) + endif() if (USE_LEVELDB) target_link_libraries(${PROJECT_NAME}server ${LEVELDB_LIBRARY}) endif() diff --git a/src/chat.h b/src/chat.h index 82ce80875..5d26baf7b 100644 --- a/src/chat.h +++ b/src/chat.h @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include <vector> #include <list> -// Chat console related classes, only used by the client +// Chat console related classes struct ChatLine { @@ -123,7 +123,7 @@ private: u32 m_scrollback; // Array of unformatted chat lines std::vector<ChatLine> m_unformatted; - + // Number of character columns in console u32 m_cols; // Number of character rows in console @@ -213,7 +213,7 @@ private: std::wstring m_line; // History buffer std::vector<std::wstring> m_history; - // History index (0 <= m_history_index <= m_history.size()) + // History index (0 <= m_history_index <= m_history.size()) u32 m_history_index; // Maximum number of history entries u32 m_history_limit; diff --git a/src/chat_interface.h b/src/chat_interface.h new file mode 100644 index 000000000..4784821fc --- /dev/null +++ b/src/chat_interface.h @@ -0,0 +1,82 @@ +/* +Minetest +Copyright (C) 2015 est31 <MTest31@outlook.com> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#ifndef CHAT_INTERFACE_H +#define CHAT_INTERFACE_H + +#include "util/container.h" +#include <string> +#include <queue> +#include "irrlichttypes.h" + +enum ChatEventType { + CET_CHAT, + CET_NICK_ADD, + CET_NICK_REMOVE, + CET_TIME_INFO, +}; + +class ChatEvent { +protected: + ChatEvent(ChatEventType a_type) { type = a_type; } +public: + ChatEventType type; +}; + +struct ChatEventTimeInfo : public ChatEvent { + ChatEventTimeInfo( + u64 a_game_time, + u32 a_time) : + ChatEvent(CET_TIME_INFO), + game_time(a_game_time), + time(a_time) + {} + + u64 game_time; + u32 time; +}; + +struct ChatEventNick : public ChatEvent { + ChatEventNick(ChatEventType a_type, + const std::string &a_nick) : + ChatEvent(a_type), // one of CET_NICK_ADD, CET_NICK_REMOVE + nick(a_nick) + {} + + std::string nick; +}; + +struct ChatEventChat : public ChatEvent { + ChatEventChat(const std::string &a_nick, + const std::wstring &an_evt_msg) : + ChatEvent(CET_CHAT), + nick(a_nick), + evt_msg(an_evt_msg) + {} + + std::string nick; + std::wstring evt_msg; +}; + +struct ChatInterface { + MutexedQueue<ChatEvent *> command_queue; // chat backend --> server + MutexedQueue<ChatEvent *> outgoing_queue; // server --> chat backend +}; + +#endif diff --git a/src/cmake_config.h.in b/src/cmake_config.h.in index bda7a891a..018532d13 100644 --- a/src/cmake_config.h.in +++ b/src/cmake_config.h.in @@ -19,12 +19,19 @@ #cmakedefine01 USE_CURL #cmakedefine01 USE_SOUND #cmakedefine01 USE_FREETYPE +#cmakedefine01 USE_CURSES #cmakedefine01 USE_LEVELDB #cmakedefine01 USE_LUAJIT #cmakedefine01 USE_SPATIAL #cmakedefine01 USE_SYSTEM_GMP #cmakedefine01 USE_REDIS #cmakedefine01 HAVE_ENDIAN_H +#cmakedefine01 CURSES_HAVE_CURSES_H +#cmakedefine01 CURSES_HAVE_NCURSES_H +#cmakedefine01 CURSES_HAVE_NCURSES_NCURSES_H +#cmakedefine01 CURSES_HAVE_NCURSES_CURSES_H +#cmakedefine01 CURSES_HAVE_NCURSESW_NCURSES_H +#cmakedefine01 CURSES_HAVE_NCURSESW_CURSES_H #endif diff --git a/src/debug.cpp b/src/debug.cpp index 3761e416d..8647160b1 100644 --- a/src/debug.cpp +++ b/src/debug.cpp @@ -37,6 +37,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "filesys.h" #endif +#if USE_CURSES + #include "terminal_chat_console.h" +#endif + /* Assert */ @@ -44,6 +48,10 @@ with this program; if not, write to the Free Software Foundation, Inc., void sanity_check_fn(const char *assertion, const char *file, unsigned int line, const char *function) { +#if USE_CURSES + g_term_console.stopAndWaitforThread(); +#endif + errorstream << std::endl << "In thread " << std::hex << thr_get_current_thread_id() << ":" << std::endl; errorstream << file << ":" << line << ": " << function @@ -57,6 +65,10 @@ void sanity_check_fn(const char *assertion, const char *file, void fatal_error_fn(const char *msg, const char *file, unsigned int line, const char *function) { +#if USE_CURSES + g_term_console.stopAndWaitforThread(); +#endif + errorstream << std::endl << "In thread " << std::hex << thr_get_current_thread_id() << ":" << std::endl; errorstream << file << ":" << line << ": " << function diff --git a/src/log.cpp b/src/log.cpp index 5cba8f700..600e715c1 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -181,6 +181,14 @@ void Logger::addOutput(ILogOutput *out, LogLevel lev) m_outputs[lev].push_back(out); } +void Logger::addOutputMasked(ILogOutput *out, LogLevelMask mask) +{ + for (size_t i = 0; i < LL_MAX; i++) { + if (mask & LOGLEVEL_TO_MASKLEVEL(i)) + m_outputs[i].push_back(out); + } +} + void Logger::addOutputMaxLevel(ILogOutput *out, LogLevel lev) { assert(lev < LL_MAX); @@ -188,15 +196,19 @@ void Logger::addOutputMaxLevel(ILogOutput *out, LogLevel lev) m_outputs[i].push_back(out); } -void Logger::removeOutput(ILogOutput *out) +LogLevelMask Logger::removeOutput(ILogOutput *out) { + LogLevelMask ret_mask = 0; for (size_t i = 0; i < LL_MAX; i++) { std::vector<ILogOutput *>::iterator it; it = std::find(m_outputs[i].begin(), m_outputs[i].end(), out); - if (it != m_outputs[i].end()) + if (it != m_outputs[i].end()) { + ret_mask |= LOGLEVEL_TO_MASKLEVEL(i); m_outputs[i].erase(it); + } } + return ret_mask; } void Logger::setLevelSilenced(LogLevel lev, bool silenced) @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include <string> #include <fstream> #include "threads.h" +#include "irrlichttypes.h" class ILogOutput; @@ -38,12 +39,16 @@ enum LogLevel { LL_MAX, }; +typedef u8 LogLevelMask; +#define LOGLEVEL_TO_MASKLEVEL(x) (1 << x) + class Logger { public: void addOutput(ILogOutput *out); void addOutput(ILogOutput *out, LogLevel lev); + void addOutputMasked(ILogOutput *out, LogLevelMask mask); void addOutputMaxLevel(ILogOutput *out, LogLevel lev); - void removeOutput(ILogOutput *out); + LogLevelMask removeOutput(ILogOutput *out); void setLevelSilenced(LogLevel lev, bool silenced); void registerThread(const std::string &name); diff --git a/src/main.cpp b/src/main.cpp index 48b1af603..5046181b5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,10 +45,15 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "httpfetch.h" #include "guiEngine.h" #include "map.h" +#include "player.h" #include "mapsector.h" #include "fontengine.h" #include "gameparams.h" #include "database.h" +#include "config.h" +#if USE_CURSES + #include "terminal_chat_console.h" +#endif #ifndef SERVER #include "client/clientlauncher.h" #endif @@ -277,6 +282,8 @@ static void set_allowed_options(OptionList *allowed_options) _("Set gameid (\"--gameid list\" prints available ones)")))); allowed_options->insert(std::make_pair("migrate", ValueSpec(VALUETYPE_STRING, _("Migrate from current map backend to another (Only works when using minetestserver or with --server)")))); + allowed_options->insert(std::make_pair("terminal", ValueSpec(VALUETYPE_FLAG, + _("Feature an interactive terminal (Only works when using minetestserver or with --server)")))); #ifndef SERVER allowed_options->insert(std::make_pair("videomodes", ValueSpec(VALUETYPE_FLAG, _("Show available video modes")))); @@ -816,21 +823,83 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings & if (cmd_args.exists("migrate")) return migrate_database(game_params, cmd_args); - try { - // Create server - Server server(game_params.world_path, game_params.game_spec, false, - bind_addr.isIPv6()); - server.start(bind_addr); - - // Run server + if (cmd_args.exists("terminal")) { +#if USE_CURSES + bool name_ok = true; + std::string admin_nick = g_settings->get("name"); + + name_ok = name_ok && !admin_nick.empty(); + name_ok = name_ok && string_allowed(admin_nick, PLAYERNAME_ALLOWED_CHARS); + + if (!name_ok) { + if (admin_nick.empty()) { + errorstream << "No name given for admin. " + << "Please check your minetest.conf that it " + << "contains a 'name = ' to your main admin account." + << std::endl; + } else { + errorstream << "Name for admin '" + << admin_nick << "' is not valid. " + << "Please check that it only contains allowed characters. " + << "Valid characters are: " << PLAYERNAME_ALLOWED_CHARS_USER_EXPL + << std::endl; + } + return false; + } + ChatInterface iface; bool &kill = *porting::signal_handler_killstatus(); - dedicated_server_loop(server, kill); - } catch (const ModError &e) { - errorstream << "ModError: " << e.what() << std::endl; - return false; - } catch (const ServerError &e) { - errorstream << "ServerError: " << e.what() << std::endl; - return false; + + try { + // Create server + Server server(game_params.world_path, + game_params.game_spec, false, bind_addr.isIPv6(), &iface); + + g_term_console.setup(&iface, &kill, admin_nick); + + g_term_console.start(); + + server.start(bind_addr); + // Run server + dedicated_server_loop(server, kill); + } catch (const ModError &e) { + g_term_console.stopAndWaitforThread(); + errorstream << "ModError: " << e.what() << std::endl; + return false; + } catch (const ServerError &e) { + g_term_console.stopAndWaitforThread(); + errorstream << "ServerError: " << e.what() << std::endl; + return false; + } + + // Tell the console to stop, and wait for it to finish, + // only then leave context and free iface + g_term_console.stop(); + g_term_console.wait(); + + g_term_console.clearKillStatus(); + } else { +#else + errorstream << "Cmd arg --terminal passed, but " + << "compiled without ncurses. Ignoring." << std::endl; + } { +#endif + try { + // Create server + Server server(game_params.world_path, game_params.game_spec, false, + bind_addr.isIPv6()); + server.start(bind_addr); + + // Run server + bool &kill = *porting::signal_handler_killstatus(); + dedicated_server_loop(server, kill); + + } catch (const ModError &e) { + errorstream << "ModError: " << e.what() << std::endl; + return false; + } catch (const ServerError &e) { + errorstream << "ServerError: " << e.what() << std::endl; + return false; + } } return true; diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp index d9ff564da..3c446e31d 100644 --- a/src/network/serverpackethandler.cpp +++ b/src/network/serverpackethandler.cpp @@ -1059,69 +1059,14 @@ void Server::handleCommand_ChatMessage(NetworkPacket* pkt) return; } - // If something goes wrong, this player is to blame - RollbackScopeActor rollback_scope(m_rollback, - std::string("player:")+player->getName()); - // Get player name of this client - std::wstring name = narrow_to_wide(player->getName()); - - // Run script hook - bool ate = m_script->on_chat_message(player->getName(), - wide_to_narrow(message)); - // If script ate the message, don't proceed - if (ate) - return; - - // Line to send to players - std::wstring line; - // Whether to send to the player that sent the line - bool send_to_sender_only = false; - - // Commands are implemented in Lua, so only catch invalid - // commands that were not "eaten" and send an error back - if (message[0] == L'/') { - message = message.substr(1); - send_to_sender_only = true; - if (message.length() == 0) - line += L"-!- Empty command"; - else - line += L"-!- Invalid command: " + str_split(message, L' ')[0]; - } - else { - if (checkPriv(player->getName(), "shout")) { - line += L"<"; - line += name; - line += L"> "; - line += message; - } else { - line += L"-!- You don't have permission to shout."; - send_to_sender_only = true; - } - } + std::string name = player->getName(); + std::wstring wname = narrow_to_wide(name); - if (line != L"") - { - /* - Send the message to sender - */ - if (send_to_sender_only) { - SendChatMessage(pkt->getPeerId(), line); - } - /* - Send the message to others - */ - else { - actionstream << "CHAT: " << wide_to_narrow(line)<<std::endl; - - std::vector<u16> clients = m_clients.getClientIDs(); - - for (std::vector<u16>::iterator i = clients.begin(); - i != clients.end(); ++i) { - if (*i != pkt->getPeerId()) - SendChatMessage(*i, line); - } - } + std::wstring answer_to_sender = handleChat(name, wname, message, pkt->getPeerId()); + if (!answer_to_sender.empty()) { + // Send the answer to sender + SendChatMessage(pkt->getPeerId(), answer_to_sender); } } diff --git a/src/player.h b/src/player.h index ec30e59d2..c11261876 100644 --- a/src/player.h +++ b/src/player.h @@ -29,6 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define PLAYERNAME_SIZE 20 #define PLAYERNAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" +#define PLAYERNAME_ALLOWED_CHARS_USER_EXPL "'a' to 'z', 'A' to 'Z', '0' to '9', '-', '_'" struct PlayerControl { diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp index 85314a3bc..59d3f5c70 100644 --- a/src/script/lua_api/l_server.cpp +++ b/src/script/lua_api/l_server.cpp @@ -45,6 +45,16 @@ int ModApiServer::l_get_server_status(lua_State *L) return 1; } +// print(text) +int ModApiServer::l_print(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + std::string text; + text = luaL_checkstring(L, 1); + getServer(L)->printToConsoleOnly(text); + return 0; +} + // chat_send_all(text) int ModApiServer::l_chat_send_all(lua_State *L) { @@ -505,6 +515,8 @@ void ModApiServer::Initialize(lua_State *L, int top) API_FCT(get_modpath); API_FCT(get_modnames); + API_FCT(print); + API_FCT(chat_send_all); API_FCT(chat_send_player); API_FCT(show_formspec); diff --git a/src/script/lua_api/l_server.h b/src/script/lua_api/l_server.h index df31f325f..06a5ddc24 100644 --- a/src/script/lua_api/l_server.h +++ b/src/script/lua_api/l_server.h @@ -46,6 +46,9 @@ private: // the returned list is sorted alphabetically for you static int l_get_modnames(lua_State *L); + // print(text) + static int l_print(lua_State *L); + // chat_send_all(text) static int l_chat_send_all(lua_State *L); diff --git a/src/script/lua_api/l_util.h b/src/script/lua_api/l_util.h index 68c24520c..6fac7e7eb 100644 --- a/src/script/lua_api/l_util.h +++ b/src/script/lua_api/l_util.h @@ -38,7 +38,7 @@ private: // log([level,] text) // Writes a line to the logger. // The one-argument version logs to infostream. - // The two-argument version accept a log level: error, action, info, or verbose. + // The two-argument version accepts a log level. static int l_log(lua_State *L); // get us precision time diff --git a/src/server.cpp b/src/server.cpp index 8c42ab5fd..6cb79c875 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -148,7 +148,8 @@ Server::Server( const std::string &path_world, const SubgameSpec &gamespec, bool simple_singleplayer_mode, - bool ipv6 + bool ipv6, + ChatInterface *iface ): m_path_world(path_world), m_gamespec(gamespec), @@ -175,6 +176,7 @@ Server::Server( m_clients(&m_con), m_shutdown_requested(false), m_shutdown_ask_reconnect(false), + m_admin_chat(iface), m_ignore_map_edit_events(false), m_ignore_map_edit_events_peer_id(0), m_next_sound_id(0) @@ -576,6 +578,36 @@ void Server::AsyncRunStep(bool initial_step) } /* + Listen to the admin chat, if available + */ + if (m_admin_chat) { + if (!m_admin_chat->command_queue.empty()) { + MutexAutoLock lock(m_env_mutex); + while (!m_admin_chat->command_queue.empty()) { + ChatEvent *evt = m_admin_chat->command_queue.pop_frontNoEx(); + if (evt->type == CET_NICK_ADD) { + // The terminal informed us of its nick choice + m_admin_nick = ((ChatEventNick *)evt)->nick; + if (!m_script->getAuth(m_admin_nick, NULL, NULL)) { + errorstream << "You haven't set up an account." << std::endl + << "Please log in using the client as '" + << m_admin_nick << "' with a secure password." << std::endl + << "Until then, you can't execute admin tasks via the console," << std::endl + << "and everybody can claim the user account instead of you," << std::endl + << "giving them full control over this server." << std::endl; + } + } else { + assert(evt->type == CET_CHAT); + handleAdminChat((ChatEventChat *)evt); + } + delete evt; + } + } + m_admin_chat->outgoing_queue.push_back( + new ChatEventTimeInfo(m_env->getGameTime(), m_env->getTimeOfDay())); + } + + /* Do background stuff */ @@ -1100,16 +1132,19 @@ PlayerSAO* Server::StageTwoClientInit(u16 peer_id) // Send information about joining in chat { - std::wstring name = L"unknown"; + std::string name = "unknown"; Player *player = m_env->getPlayer(peer_id); if(player != NULL) - name = narrow_to_wide(player->getName()); + name = player->getName(); std::wstring message; message += L"*** "; - message += name; + message += narrow_to_wide(name); message += L" joined the game."; SendChatMessage(PEER_ID_INEXISTENT,message); + if (m_admin_chat) + m_admin_chat->outgoing_queue.push_back( + new ChatEventNick(CET_NICK_ADD, name)); } } Address addr = getPeerAddress(player->peer_id); @@ -1432,6 +1467,16 @@ void Server::handlePeerChanges() } } +void Server::printToConsoleOnly(const std::string &text) +{ + if (m_admin_chat) { + m_admin_chat->outgoing_queue.push_back( + new ChatEventChat("", utf8_to_wide(text))); + } else { + std::cout << text; + } +} + void Server::Send(NetworkPacket* pkt) { m_clients.send(pkt->getPeerId(), @@ -2665,9 +2710,13 @@ void Server::DeleteClient(u16 peer_id, ClientDeletionReason reason) os << player->getName() << " "; } - actionstream << player->getName() << " " + std::string name = player->getName(); + actionstream << name << " " << (reason == CDR_TIMEOUT ? "times out." : "leaves game.") << " List of players: " << os.str() << std::endl; + if (m_admin_chat) + m_admin_chat->outgoing_queue.push_back( + new ChatEventNick(CET_NICK_REMOVE, name)); } } { @@ -2700,6 +2749,77 @@ void Server::UpdateCrafting(Player* player) plist->changeItem(0, preview); } +std::wstring Server::handleChat(const std::string &name, const std::wstring &wname, + const std::wstring &wmessage, u16 peer_id_to_avoid_sending) +{ + // If something goes wrong, this player is to blame + RollbackScopeActor rollback_scope(m_rollback, + std::string("player:") + name); + + // Line to send + std::wstring line; + // Whether to send line to the player that sent the message, or to all players + bool broadcast_line = true; + + // Run script hook + bool ate = m_script->on_chat_message(name, + wide_to_utf8(wmessage)); + // If script ate the message, don't proceed + if (ate) + return L""; + + // Commands are implemented in Lua, so only catch invalid + // commands that were not "eaten" and send an error back + if (wmessage[0] == L'/') { + std::wstring wcmd = wmessage.substr(1); + broadcast_line = false; + if (wcmd.length() == 0) + line += L"-!- Empty command"; + else + line += L"-!- Invalid command: " + str_split(wcmd, L' ')[0]; + } else { + line += L"<"; + line += wname; + line += L"> "; + line += wmessage; + } + + /* + Tell calling method to send the message to sender + */ + if (!broadcast_line) { + return line; + } else { + /* + Send the message to others + */ + actionstream << "CHAT: " << wide_to_narrow(line) << std::endl; + + std::vector<u16> clients = m_clients.getClientIDs(); + + for (u16 i = 0; i < clients.size(); i++) { + u16 cid = clients[i]; + if (cid != peer_id_to_avoid_sending) + SendChatMessage(cid, line); + } + } + return L""; +} + +void Server::handleAdminChat(const ChatEventChat *evt) +{ + std::string name = evt->nick; + std::wstring wname = utf8_to_wide(name); + std::wstring wmessage = evt->evt_msg; + + std::wstring answer = handleChat(name, wname, wmessage); + + // If asked to send answer to sender + if (!answer.empty()) { + m_admin_chat->outgoing_queue.push_back(new ChatEventChat("", answer)); + } +} + RemoteClient* Server::getClient(u16 peer_id, ClientState state_min) { RemoteClient *client = getClientNoEx(peer_id,state_min); @@ -2831,9 +2951,14 @@ void Server::notifyPlayer(const char *name, const std::wstring &msg) if (!m_env) return; + if (m_admin_nick == name && !m_admin_nick.empty()) { + m_admin_chat->outgoing_queue.push_back(new ChatEventChat("", msg)); + } + Player *player = m_env->getPlayer(name); - if (!player) + if (!player) { return; + } if (player->peer_id == PEER_ID_INEXISTENT) return; diff --git a/src/server.h b/src/server.h index bee978de2..6d66c9386 100644 --- a/src/server.h +++ b/src/server.h @@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "util/numeric.h" #include "util/thread.h" #include "environment.h" +#include "chat_interface.h" #include "clientiface.h" #include "network/networkpacket.h" #include <string> @@ -171,7 +172,8 @@ public: const std::string &path_world, const SubgameSpec &gamespec, bool simple_singleplayer_mode, - bool ipv6 + bool ipv6, + ChatInterface *iface = NULL ); ~Server(); void start(Address bind_addr); @@ -369,6 +371,8 @@ public: u8* ser_vers, u16* prot_vers, u8* major, u8* minor, u8* patch, std::string* vers_string); + void printToConsoleOnly(const std::string &text); + void SendPlayerHPOrDie(PlayerSAO *player); void SendPlayerBreath(u16 peer_id); void SendInventory(PlayerSAO* playerSAO); @@ -472,6 +476,12 @@ private: void DeleteClient(u16 peer_id, ClientDeletionReason reason); void UpdateCrafting(Player *player); + // This returns the answer to the sender of wmessage, or "" if there is none + std::wstring handleChat(const std::string &name, const std::wstring &wname, + const std::wstring &wmessage, + u16 peer_id_to_avoid_sending = PEER_ID_INEXISTENT); + void handleAdminChat(const ChatEventChat *evt); + v3f findSpawnPos(); // When called, connection mutex should be locked @@ -597,6 +607,9 @@ private: std::string m_shutdown_msg; bool m_shutdown_ask_reconnect; + ChatInterface *m_admin_chat; + std::string m_admin_nick; + /* Map edit event queue. Automatically receives all map edits. The constructor of this class registers us to receive them through diff --git a/src/terminal_chat_console.cpp b/src/terminal_chat_console.cpp new file mode 100644 index 000000000..ac06285eb --- /dev/null +++ b/src/terminal_chat_console.cpp @@ -0,0 +1,452 @@ +/* +Minetest +Copyright (C) 2015 est31 <MTest31@outlook.com> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "config.h" +#if USE_CURSES +#include "version.h" +#include "terminal_chat_console.h" +#include "porting.h" +#include "settings.h" +#include "util/numeric.h" +#include "util/string.h" + +TerminalChatConsole g_term_console; + +// include this last to avoid any conflicts +// (likes to set macros to common names, conflicting various stuff) +#if CURSES_HAVE_NCURSESW_NCURSES_H +#include <ncursesw/ncurses.h> +#elif CURSES_HAVE_NCURSESW_CURSES_H +#include <ncursesw/curses.h> +#elif CURSES_HAVE_CURSES_H +#include <curses.h> +#elif CURSES_HAVE_NCURSES_H +#include <ncurses.h> +#elif CURSES_HAVE_NCURSES_NCURSES_H +#include <ncurses/ncurses.h> +#elif CURSES_HAVE_NCURSES_CURSES_H +#include <ncurses/curses.h> +#endif + +// Some functions to make drawing etc position independent +static bool reformat_backend(ChatBackend *backend, int rows, int cols) +{ + if (rows < 2) + return false; + backend->reformat(cols, rows - 2); + return true; +} + +static void move_for_backend(int row, int col) +{ + move(row + 1, col); +} + +void TerminalChatConsole::initOfCurses() +{ + initscr(); + cbreak(); //raw(); + noecho(); + keypad(stdscr, TRUE); + nodelay(stdscr, TRUE); + timeout(100); + + // To make esc not delay up to one second. According to the internet, + // this is the value vim uses, too. + set_escdelay(25); + + getmaxyx(stdscr, m_rows, m_cols); + m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols); +} + +void TerminalChatConsole::deInitOfCurses() +{ + endwin(); +} + +void *TerminalChatConsole::run() +{ + BEGIN_DEBUG_EXCEPTION_HANDLER + + std::cout << "========================" << std::endl; + std::cout << "Begin log output over terminal" + << " (no stdout/stderr backlog during that)" << std::endl; + // Make the loggers to stdout/stderr shut up. + // Go over our own loggers instead. + LogLevelMask err_mask = g_logger.removeOutput(&stderr_output); + LogLevelMask out_mask = g_logger.removeOutput(&stdout_output); + + g_logger.addOutput(&m_log_output); + + // Inform the server of our nick + m_chat_interface->command_queue.push_back( + new ChatEventNick(CET_NICK_ADD, m_nick)); + + { + // Ensures that curses is deinitialized even on an exception being thrown + CursesInitHelper helper(this); + + while (!stopRequested()) { + + int ch = getch(); + if (stopRequested()) + break; + + step(ch); + } + } + + if (m_kill_requested) + *m_kill_requested = true; + + g_logger.removeOutput(&m_log_output); + g_logger.addOutputMasked(&stderr_output, err_mask); + g_logger.addOutputMasked(&stdout_output, out_mask); + + std::cout << "End log output over terminal" + << " (no stdout/stderr backlog during that)" << std::endl; + std::cout << "========================" << std::endl; + + END_DEBUG_EXCEPTION_HANDLER + + return NULL; +} + +void TerminalChatConsole::typeChatMessage(const std::wstring &msg) +{ + // Discard empty line + if (msg.empty()) + return; + + // Send to server + m_chat_interface->command_queue.push_back( + new ChatEventChat(m_nick, msg)); + + // Print if its a command (gets eaten by server otherwise) + if (msg[0] == L'/') { + m_chat_backend.addMessage(L"", (std::wstring)L"Issued command: " + msg); + } +} + +void TerminalChatConsole::handleInput(int ch, bool &complete_redraw_needed) +{ + // Helpful if you want to collect key codes that aren't documented + /*if (ch != ERR) { + m_chat_backend.addMessage(L"", + (std::wstring)L"Pressed key " + utf8_to_wide( + std::string(keyname(ch)) + " (code " + itos(ch) + ")")); + complete_redraw_needed = true; + }//*/ + + // All the key codes below are compatible to xterm + // Only add new ones if you have tried them there, + // to ensure compatibility with not just xterm but the wide + // range of terminals that are compatible to xterm. + + switch (ch) { + case ERR: // no input + break; + case 27: // ESC + // Toggle ESC mode + m_esc_mode = !m_esc_mode; + break; + case KEY_PPAGE: + m_chat_backend.scrollPageUp(); + complete_redraw_needed = true; + break; + case KEY_NPAGE: + m_chat_backend.scrollPageDown(); + complete_redraw_needed = true; + break; + case KEY_ENTER: + case '\r': + case '\n': { + std::wstring text = m_chat_backend.getPrompt().submit(); + typeChatMessage(text); + break; + } + case KEY_UP: + m_chat_backend.getPrompt().historyPrev(); + break; + case KEY_DOWN: + m_chat_backend.getPrompt().historyNext(); + break; + case KEY_LEFT: + // Left pressed + // move character to the left + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_LEFT, + ChatPrompt::CURSOROP_SCOPE_CHARACTER); + break; + case 545: + // Ctrl-Left pressed + // move word to the left + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_LEFT, + ChatPrompt::CURSOROP_SCOPE_WORD); + break; + case KEY_RIGHT: + // Right pressed + // move character to the right + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_CHARACTER); + break; + case 560: + // Ctrl-Right pressed + // move word to the right + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_WORD); + break; + case KEY_HOME: + // Home pressed + // move to beginning of line + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_LEFT, + ChatPrompt::CURSOROP_SCOPE_LINE); + break; + case KEY_END: + // End pressed + // move to end of line + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_MOVE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_LINE); + break; + case KEY_BACKSPACE: + case '\b': + case 127: + // Backspace pressed + // delete character to the left + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, + ChatPrompt::CURSOROP_SCOPE_CHARACTER); + break; + case KEY_DC: + // Delete pressed + // delete character to the right + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_CHARACTER); + break; + case 519: + // Ctrl-Delete pressed + // delete word to the right + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_WORD); + break; + case 21: + // Ctrl-U pressed + // kill line to left end + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_LEFT, + ChatPrompt::CURSOROP_SCOPE_LINE); + break; + case 11: + // Ctrl-K pressed + // kill line to right end + m_chat_backend.getPrompt().cursorOperation( + ChatPrompt::CURSOROP_DELETE, + ChatPrompt::CURSOROP_DIR_RIGHT, + ChatPrompt::CURSOROP_SCOPE_LINE); + break; + case KEY_TAB: + // Tab pressed + // Nick completion + m_chat_backend.getPrompt().nickCompletion(m_nicks, false); + break; + default: + // Add character to the prompt, + // assuming UTF-8. + if (IS_UTF8_MULTB_START(ch)) { + m_pending_utf8_bytes.append(1, (char)ch); + m_utf8_bytes_to_wait += UTF8_MULTB_START_LEN(ch) - 1; + } else if (m_utf8_bytes_to_wait != 0) { + m_pending_utf8_bytes.append(1, (char)ch); + m_utf8_bytes_to_wait--; + if (m_utf8_bytes_to_wait == 0) { + std::wstring w = utf8_to_wide(m_pending_utf8_bytes); + m_pending_utf8_bytes = ""; + // hopefully only one char in the wstring... + for (size_t i = 0; i < w.size(); i++) { + m_chat_backend.getPrompt().input(w.c_str()[i]); + } + } + } else if (IS_ASCII_PRINTABLE_CHAR(ch)) { + m_chat_backend.getPrompt().input(ch); + } else { + // Silently ignore characters we don't handle + + //warningstream << "Pressed invalid character '" + // << keyname(ch) << "' (code " << itos(ch) << ")" << std::endl; + } + break; + } +} + +void TerminalChatConsole::step(int ch) +{ + bool complete_redraw_needed = false; + + // empty queues + while (!m_chat_interface->outgoing_queue.empty()) { + ChatEvent *evt = m_chat_interface->outgoing_queue.pop_frontNoEx(); + switch (evt->type) { + case CET_NICK_REMOVE: + m_nicks.remove(((ChatEventNick *)evt)->nick); + break; + case CET_NICK_ADD: + m_nicks.push_back(((ChatEventNick *)evt)->nick); + break; + case CET_CHAT: + complete_redraw_needed = true; + // This is only used for direct replies from commands + // or for lua's print() functionality + m_chat_backend.addMessage(L"", ((ChatEventChat *)evt)->evt_msg); + break; + case CET_TIME_INFO: + ChatEventTimeInfo *tevt = (ChatEventTimeInfo *)evt; + m_game_time = tevt->game_time; + m_time_of_day = tevt->time; + }; + delete evt; + } + while (!m_log_output.queue.empty()) { + complete_redraw_needed = true; + std::pair<LogLevel, std::string> p = m_log_output.queue.pop_frontNoEx(); + if (p.first > m_log_level) + continue; + + m_chat_backend.addMessage( + utf8_to_wide(Logger::getLevelLabel(p.first)), + utf8_to_wide(p.second)); + } + + // handle input + if (!m_esc_mode) { + handleInput(ch, complete_redraw_needed); + } else { + switch (ch) { + case ERR: // no input + break; + case 27: // ESC + // Toggle ESC mode + m_esc_mode = !m_esc_mode; + break; + case 'L': + m_log_level--; + m_log_level = MYMAX(m_log_level, LL_NONE + 1); // LL_NONE isn't accessible + break; + case 'l': + m_log_level++; + m_log_level = MYMIN(m_log_level, LL_MAX - 1); + break; + } + } + + // was there a resize? + int xn, yn; + getmaxyx(stdscr, yn, xn); + if (xn != m_cols || yn != m_rows) { + m_cols = xn; + m_rows = yn; + m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols); + complete_redraw_needed = true; + } + + // draw title + move(0, 0); + clrtoeol(); + addstr(PROJECT_NAME_C); + addstr(" "); + addstr(g_version_hash); + + u32 minutes = m_time_of_day % 1000; + u32 hours = m_time_of_day / 1000; + minutes = (float)minutes / 1000 * 60; + + if (m_game_time) + printw(" | Game %d Time of day %02d:%02d ", + m_game_time, hours, minutes); + + // draw text + if (complete_redraw_needed && m_can_draw_text) + draw_text(); + + // draw prompt + if (!m_esc_mode) { + // normal prompt + ChatPrompt& prompt = m_chat_backend.getPrompt(); + std::string prompt_text = wide_to_utf8(prompt.getVisiblePortion()); + move(m_rows - 1, 0); + clrtoeol(); + addstr(prompt_text.c_str()); + // Draw cursor + s32 cursor_pos = prompt.getVisibleCursorPosition(); + if (cursor_pos >= 0) { + move(m_rows - 1, cursor_pos); + } + } else { + // esc prompt + move(m_rows - 1, 0); + clrtoeol(); + printw("[ESC] Toggle ESC mode |" + " [CTRL+C] Shut down |" + " (L) in-, (l) decrease loglevel %s", + Logger::getLevelLabel((LogLevel) m_log_level).c_str()); + } + + refresh(); +} + +void TerminalChatConsole::draw_text() +{ + ChatBuffer& buf = m_chat_backend.getConsoleBuffer(); + for (u32 row = 0; row < buf.getRows(); row++) { + move_for_backend(row, 0); + clrtoeol(); + const ChatFormattedLine& line = buf.getFormattedLine(row); + if (line.fragments.empty()) + continue; + for (u32 i = 0; i < line.fragments.size(); ++i) { + const ChatFormattedFragment& fragment = line.fragments[i]; + addstr(wide_to_utf8(fragment.text).c_str()); + } + } +} + +void TerminalChatConsole::stopAndWaitforThread() +{ + clearKillStatus(); + stop(); + wait(); +} + +#endif diff --git a/src/terminal_chat_console.h b/src/terminal_chat_console.h new file mode 100644 index 000000000..2111b7ecb --- /dev/null +++ b/src/terminal_chat_console.h @@ -0,0 +1,131 @@ +/* +Minetest +Copyright (C) 2015 est31 <MTest31@outlook.com> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#ifndef TERMINAL_CHAT_CONSOLE_H +#define TERMINAL_CHAT_CONSOLE_H + +#include "chat.h" +#include "threading/thread.h" +#include "chat_interface.h" +#include "log.h" + +#include <sstream> + +class TermLogOutput : public ILogOutput { +public: + + void logRaw(LogLevel lev, const std::string &line) + { + queue.push_back(std::make_pair(lev, line)); + } + + virtual void log(LogLevel lev, const std::string &combined, + const std::string &time, const std::string &thread_name, + const std::string &payload_text) + { + std::ostringstream os(std::ios_base::binary); + os << time << ": [" << thread_name << "] " << payload_text; + + queue.push_back(std::make_pair(lev, os.str())); + } + + MutexedQueue<std::pair<LogLevel, std::string> > queue; +}; + +class TerminalChatConsole : public Thread { +public: + + TerminalChatConsole() : + Thread("TerminalThread"), + m_log_level(LL_ACTION), + m_utf8_bytes_to_wait(0), + m_kill_requested(NULL), + m_esc_mode(false), + m_game_time(0), + m_time_of_day(0) + {} + + void setup( + ChatInterface *iface, + bool *kill_requested, + const std::string &nick) + { + m_nick = nick; + m_kill_requested = kill_requested; + m_chat_interface = iface; + } + + virtual void *run(); + + // Highly required! + void clearKillStatus() { m_kill_requested = NULL; } + + void stopAndWaitforThread(); + +private: + // these have stupid names so that nobody missclassifies them + // as curses functions. Oh, curses has stupid names too? + // Well, at least it was worth a try... + void initOfCurses(); + void deInitOfCurses(); + + void draw_text(); + + void typeChatMessage(const std::wstring &m); + + void handleInput(int ch, bool &complete_redraw_needed); + + void step(int ch); + + // Used to ensure the deinitialisation is always called. + struct CursesInitHelper { + TerminalChatConsole *cons; + CursesInitHelper(TerminalChatConsole * a_console) + : cons(a_console) + { cons->initOfCurses(); } + ~CursesInitHelper() { cons->deInitOfCurses(); } + }; + + int m_log_level; + std::string m_nick; + + u8 m_utf8_bytes_to_wait; + std::string m_pending_utf8_bytes; + + std::list<std::string> m_nicks; + + int m_cols; + int m_rows; + bool m_can_draw_text; + + bool *m_kill_requested; + ChatBackend m_chat_backend; + ChatInterface *m_chat_interface; + + TermLogOutput m_log_output; + + bool m_esc_mode; + + u64 m_game_time; + u32 m_time_of_day; +}; + +extern TerminalChatConsole g_term_console; + +#endif diff --git a/src/unittest/test_utilities.cpp b/src/unittest/test_utilities.cpp index 3c000e760..1785997de 100644 --- a/src/unittest/test_utilities.cpp +++ b/src/unittest/test_utilities.cpp @@ -43,6 +43,7 @@ public: void testStrToIntConversion(); void testStringReplace(); void testStringAllowed(); + void testAsciiPrintableHelper(); void testUTF8(); void testWrapRows(); void testIsNumber(); @@ -68,6 +69,7 @@ void TestUtilities::runTests(IGameDef *gamedef) TEST(testStrToIntConversion); TEST(testStringReplace); TEST(testStringAllowed); + TEST(testAsciiPrintableHelper); TEST(testUTF8); TEST(testWrapRows); TEST(testIsNumber); @@ -232,6 +234,18 @@ void TestUtilities::testStringAllowed() UASSERT(string_allowed_blacklist("hello123", "123") == false); } +void TestUtilities::testAsciiPrintableHelper() +{ + UASSERT(IS_ASCII_PRINTABLE_CHAR('e') == true); + UASSERT(IS_ASCII_PRINTABLE_CHAR('\0') == false); + + // Ensures that there is no cutting off going on... + // If there were, 331 would be cut to 75 in this example + // and 73 is a valid ASCII char. + int ch = 331; + UASSERT(IS_ASCII_PRINTABLE_CHAR(ch) == false); +} + void TestUtilities::testUTF8() { UASSERT(wide_to_utf8(utf8_to_wide("")) == ""); diff --git a/src/util/string.h b/src/util/string.h index 793baad0e..c8f60b802 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -32,8 +32,26 @@ with this program; if not, write to the Free Software Foundation, Inc., #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) +// Checks whether a value is an ASCII printable character +#define IS_ASCII_PRINTABLE_CHAR(x) \ + (((unsigned int)(x) >= 0x20) && \ + ( (unsigned int)(x) <= 0x7e)) + // Checks whether a byte is an inner byte for an utf-8 multibyte sequence -#define IS_UTF8_MULTB_INNER(x) (((unsigned char)x >= 0x80) && ((unsigned char)x < 0xc0)) +#define IS_UTF8_MULTB_INNER(x) \ + (((unsigned char)(x) >= 0x80) && \ + ( (unsigned char)(x) <= 0xbf)) + +// Checks whether a byte is a start byte for an utf-8 multibyte sequence +#define IS_UTF8_MULTB_START(x) \ + (((unsigned char)(x) >= 0xc2) && \ + ( (unsigned char)(x) <= 0xf4)) + +// Given a start byte x for an utf-8 multibyte sequence +// it gives the length of the whole sequence in bytes. +#define UTF8_MULTB_START_LEN(x) \ + (((unsigned char)(x) < 0xe0) ? 2 : \ + (((unsigned char)(x) < 0xf0) ? 3 : 4)) typedef std::map<std::string, std::string> StringMap; |